-
-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #216 from stefanprodan/bundle-status
Add `timoni bundle status` command
- Loading branch information
Showing
7 changed files
with
388 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
}) | ||
} |
Oops, something went wrong.