Skip to content

Commit

Permalink
Merge pull request #216 from stefanprodan/bundle-status
Browse files Browse the repository at this point in the history
Add `timoni bundle status` command
  • Loading branch information
stefanprodan authored Oct 18, 2023
2 parents 1438c1e + f7caedc commit db8e2cc
Show file tree
Hide file tree
Showing 7 changed files with 388 additions and 29 deletions.
133 changes: 133 additions & 0 deletions cmd/timoni/bundle_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
Copyright 2023 Stefan Prodan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"context"
"errors"
"fmt"

"cuelang.org/go/cue/cuecontext"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/stefanprodan/timoni/internal/engine"
"github.com/stefanprodan/timoni/internal/runtime"

apiv1 "github.com/stefanprodan/timoni/api/v1alpha1"
)

var bundleStatusCmd = &cobra.Command{
Use: "status [BUNDLE NAME]",
Short: "Displays the current status of Kubernetes resources managed by the bundle instances",
Example: ` # Show the status of the resources managed by a bundle
timoni bundle status -f bundle.cue
# Show the status using a named bundle
timoni bundle status my-app
`,
RunE: runBundleStatusCmd,
}

type bundleStatusFlags struct {
name string
filename string
}

var bundleStatusArgs bundleStatusFlags

func init() {
bundleStatusCmd.Flags().StringVarP(&bundleStatusArgs.filename, "file", "f", "",
"The local path to bundle.cue file.")
bundleCmd.AddCommand(bundleStatusCmd)
}

func runBundleStatusCmd(cmd *cobra.Command, args []string) error {
if len(args) < 1 && bundleStatusArgs.filename == "" {
return fmt.Errorf("bundle name is required")
}

switch {
case bundleStatusArgs.filename != "":
cuectx := cuecontext.New()
name, err := engine.ExtractStringFromFile(cuectx, bundleStatusArgs.filename, apiv1.BundleName.String())
if err != nil {
return err
}
bundleStatusArgs.name = name
default:
bundleStatusArgs.name = args[0]
}

rm, err := runtime.NewResourceManager(kubeconfigArgs)
if err != nil {
return err
}

ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()

sm := runtime.NewStorageManager(rm)
instances, err := sm.List(ctx, "", bundleStatusArgs.name)
if err != nil {
return err
}

if len(instances) == 0 {
return fmt.Errorf("no instances found in bundle")
}

for _, instance := range instances {
log := LoggerBundleInstance(ctx, bundleStatusArgs.name, instance.Name)

log.Info(fmt.Sprintf("last applied %s",
colorizeSubject(instance.LastTransitionTime)))
log.Info(fmt.Sprintf("module %s",
colorizeSubject(instance.Module.Repository+":"+instance.Module.Version)))
log.Info(fmt.Sprintf("digest %s",
colorizeSubject(instance.Module.Digest)))

im := runtime.InstanceManager{Instance: apiv1.Instance{Inventory: instance.Inventory}}
objects, err := im.ListObjects()
if err != nil {
return err
}

for _, obj := range objects {
err = rm.Client().Get(ctx, client.ObjectKeyFromObject(obj), obj)
if err != nil {
if apierrors.IsNotFound(err) {
log.Error(err, colorizeJoin(obj, errors.New("NotFound")))
continue
}
log.Error(err, colorizeJoin(obj, errors.New("Unknown")))
continue
}

res, err := status.Compute(obj)
if err != nil {
log.Error(err, colorizeJoin(obj, errors.New("Failed")))
continue
}
log.Info(colorizeJoin(obj, res.Status, "-", res.Message))
}
}

return nil
}
115 changes: 115 additions & 0 deletions cmd/timoni/bundle_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
Copyright 2023 Stefan Prodan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"context"
"fmt"
"strings"
"testing"

. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func Test_BundleStatus(t *testing.T) {
g := NewWithT(t)

bundleName := "my-bundle"
modPath := "testdata/module"
namespace := rnd("my-namespace", 5)
modName := rnd("my-mod", 5)
modURL := fmt.Sprintf("%s/%s", dockerRegistry, modName)
modVer := "1.0.0"

_, err := executeCommand(fmt.Sprintf(
"mod push %s oci://%s -v %s",
modPath,
modURL,
modVer,
))
g.Expect(err).ToNot(HaveOccurred())

bundleData := fmt.Sprintf(`
bundle: {
apiVersion: "v1alpha1"
name: "%[1]s"
instances: {
frontend: {
module: {
url: "oci://%[2]s"
version: "%[3]s"
}
namespace: "%[4]s"
values: server: enabled: false
}
backend: {
module: {
url: "oci://%[2]s"
version: "%[3]s"
}
namespace: "%[4]s"
values: client: enabled: false
}
}
}
`, bundleName, modURL, modVer, namespace)

_, err = executeCommandWithIn("bundle apply -f - -p main --wait", strings.NewReader(bundleData))
g.Expect(err).ToNot(HaveOccurred())

t.Run("lists modules", func(t *testing.T) {
g := NewWithT(t)

output, err := executeCommand(fmt.Sprintf("bundle status %s", bundleName))
g.Expect(err).ToNot(HaveOccurred())
g.Expect(output).To(ContainSubstring(fmt.Sprintf("oci://%s:%s", modURL, modVer)))
g.Expect(output).To(ContainSubstring("digest sha256:"))
})

t.Run("lists ready resources", func(t *testing.T) {
g := NewWithT(t)

output, err := executeCommand(fmt.Sprintf("bundle status %s", bundleName))
g.Expect(err).ToNot(HaveOccurred())
g.Expect(output).To(ContainSubstring(fmt.Sprintf("ConfigMap/%s/frontend-client Current", namespace)))
g.Expect(output).To(ContainSubstring(fmt.Sprintf("ConfigMap/%s/backend-server Current", namespace)))
})

t.Run("lists not found resources", func(t *testing.T) {
g := NewWithT(t)

cm := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "v1",
APIVersion: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: "backend-server",
Namespace: namespace,
},
}
err = envTestClient.Delete(context.Background(), cm)
g.Expect(err).ToNot(HaveOccurred())

output, err := executeCommand(fmt.Sprintf("bundle status %s", bundleName))
g.Expect(err).ToNot(HaveOccurred())
g.Expect(output).To(ContainSubstring(fmt.Sprintf("ConfigMap/%s/frontend-client Current", namespace)))
g.Expect(output).To(ContainSubstring(fmt.Sprintf("ConfigMap/%s/backend-server NotFound", namespace)))
})
}
1 change: 1 addition & 0 deletions cmd/timoni/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func resetCmdArgs() {
applyArgs = applyFlags{}
buildArgs = buildFlags{}
deleteArgs = deleteFlags{}
statusArgs = statusFlags{}
inspectModuleArgs = inspectModuleFlags{}
inspectResourcesArgs = inspectResourcesFlags{}
inspectValuesArgs = inspectValuesFlags{}
Expand Down
10 changes: 5 additions & 5 deletions cmd/timoni/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ var statusCmd = &cobra.Command{
Use: "status [INSTANCE NAME]",
Short: "Displays the current status of Kubernetes resources managed by an instance",
Example: ` # Show the current status of the managed resources
timoni status -n apps app
timoni -n apps status app
`,
RunE: runstatusCmd,
RunE: runStatusCmd,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
case 0:
Expand All @@ -58,7 +58,7 @@ func init() {
rootCmd.AddCommand(statusCmd)
}

func runstatusCmd(cmd *cobra.Command, args []string) error {
func runStatusCmd(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("instance name is required")
}
Expand Down Expand Up @@ -94,13 +94,13 @@ func runstatusCmd(cmd *cobra.Command, args []string) error {
log.Error(err, colorizeJoin(obj, errors.New("NotFound")))
continue
}
log.Error(err, colorizeJoin(obj, errors.New("query failed")))
log.Error(err, colorizeJoin(obj, errors.New("Unknown")))
continue
}

res, err := status.Compute(obj)
if err != nil {
log.Error(err, colorizeJoin(obj, errors.New("statusFailed failed")))
log.Error(err, colorizeJoin(obj, errors.New("Failed")))
continue
}
log.Info(colorizeJoin(obj, res.Status, "-", res.Message))
Expand Down
98 changes: 98 additions & 0 deletions cmd/timoni/status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
Copyright 2023 Stefan Prodan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"context"
"fmt"
"strings"
"testing"

. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestInstanceStatus(t *testing.T) {
g := NewWithT(t)
modPath := "testdata/module"
modURL := fmt.Sprintf("oci://%s/%s", dockerRegistry, rnd("my-status", 5))
modVer := "1.0.0"
name := rnd("my-instance", 5)
namespace := rnd("my-namespace", 5)

// Package the module as an OCI artifact and push it to registry
_, err := executeCommand(fmt.Sprintf(
"mod push %s %s -v %s",
modPath,
modURL,
modVer,
))
g.Expect(err).ToNot(HaveOccurred())

// Install the module from the registry
_, err = executeCommandWithIn(fmt.Sprintf(
"apply -n %s %s %s -v %s -p main --wait -f-",
namespace,
name,
modURL,
modVer,
), strings.NewReader(`values: domain: "app.internal"`))
g.Expect(err).ToNot(HaveOccurred())

t.Run("ready status", func(t *testing.T) {
g := NewWithT(t)
output, err := executeCommand(fmt.Sprintf(
"status -n %s %s",
namespace,
name,
))
g.Expect(err).ToNot(HaveOccurred())

// Verify status output contains the expected resources
g.Expect(output).To(ContainSubstring(fmt.Sprintf("ConfigMap/%s/%s-client Current", namespace, name)))
g.Expect(output).To(ContainSubstring(fmt.Sprintf("ConfigMap/%s/%s-server Current", namespace, name)))
})

t.Run("not found status", func(t *testing.T) {
g := NewWithT(t)

cm := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "v1",
APIVersion: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: name + "-server",
Namespace: namespace,
},
}
err = envTestClient.Delete(context.Background(), cm)
g.Expect(err).ToNot(HaveOccurred())

output, err := executeCommand(fmt.Sprintf(
"status -n %s %s",
namespace,
name,
))
g.Expect(err).ToNot(HaveOccurred())

// Verify status output contains the expected resources
g.Expect(output).To(ContainSubstring(fmt.Sprintf("ConfigMap/%s/%s-client Current", namespace, name)))
g.Expect(output).To(ContainSubstring(fmt.Sprintf("ConfigMap/%s/%s-server NotFound", namespace, name)))
})
}
Loading

0 comments on commit db8e2cc

Please sign in to comment.