mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-04 14:08:52 +00:00
Compare commits
6 Commits
main
...
feat/cozyc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
488b1bf27b | ||
|
|
f4e9660b43 | ||
|
|
d9cfd5ac9e | ||
|
|
5762ac4139 | ||
|
|
38f446c0d3 | ||
|
|
4de8e91864 |
15
Makefile
15
Makefile
@@ -58,7 +58,10 @@ manifests:
|
||||
cozypkg:
|
||||
go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozypkg/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozypkg ./cmd/cozypkg
|
||||
|
||||
assets: assets-talos assets-cozypkg
|
||||
cozyctl:
|
||||
go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozyctl/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozyctl ./cmd/cozyctl
|
||||
|
||||
assets: assets-talos assets-cozypkg assets-cozyctl
|
||||
|
||||
assets-talos:
|
||||
make -C packages/core/talos assets
|
||||
@@ -73,6 +76,16 @@ assets-cozypkg-%:
|
||||
cp LICENSE _out/bin/cozypkg-$*/LICENSE
|
||||
tar -C _out/bin/cozypkg-$* -czf _out/assets/cozypkg-$*.tar.gz LICENSE cozypkg$(EXT)
|
||||
|
||||
assets-cozyctl: assets-cozyctl-linux-amd64 assets-cozyctl-linux-arm64 assets-cozyctl-darwin-amd64 assets-cozyctl-darwin-arm64 assets-cozyctl-windows-amd64 assets-cozyctl-windows-arm64
|
||||
(cd _out/assets/ && sha256sum cozyctl-*.tar.gz) > _out/assets/cozyctl-checksums.txt
|
||||
|
||||
assets-cozyctl-%:
|
||||
$(eval EXT := $(if $(filter windows,$(firstword $(subst -, ,$*))),.exe,))
|
||||
mkdir -p _out/assets
|
||||
GOOS=$(firstword $(subst -, ,$*)) GOARCH=$(lastword $(subst -, ,$*)) go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozyctl/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozyctl-$*/cozyctl$(EXT) ./cmd/cozyctl
|
||||
cp LICENSE _out/bin/cozyctl-$*/LICENSE
|
||||
tar -C _out/bin/cozyctl-$* -czf _out/assets/cozyctl-$*.tar.gz LICENSE cozyctl$(EXT)
|
||||
|
||||
test:
|
||||
make -C packages/core/testing apply
|
||||
make -C packages/core/testing test
|
||||
|
||||
114
cmd/cozyctl/cmd/client.go
Normal file
114
cmd/cozyctl/cmd/client.go
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func buildRestConfig() (*rest.Config, error) {
|
||||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
if globalFlags.kubeconfig != "" {
|
||||
rules.ExplicitPath = globalFlags.kubeconfig
|
||||
}
|
||||
overrides := &clientcmd.ConfigOverrides{}
|
||||
if globalFlags.context != "" {
|
||||
overrides.CurrentContext = globalFlags.context
|
||||
}
|
||||
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides).ClientConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load kubeconfig: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func newScheme() *runtime.Scheme {
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
return scheme
|
||||
}
|
||||
|
||||
func newClients() (client.Client, dynamic.Interface, error) {
|
||||
config, err := buildRestConfig()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
scheme := newScheme()
|
||||
|
||||
typedClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
dynClient, err := dynamic.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create dynamic client: %w", err)
|
||||
}
|
||||
|
||||
return typedClient, dynClient, nil
|
||||
}
|
||||
|
||||
func getNamespace() (string, error) {
|
||||
if globalFlags.namespace != "" {
|
||||
return globalFlags.namespace, nil
|
||||
}
|
||||
|
||||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
if globalFlags.kubeconfig != "" {
|
||||
rules.ExplicitPath = globalFlags.kubeconfig
|
||||
}
|
||||
overrides := &clientcmd.ConfigOverrides{}
|
||||
if globalFlags.context != "" {
|
||||
overrides.CurrentContext = globalFlags.context
|
||||
}
|
||||
|
||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
|
||||
ns, _, err := clientConfig.Namespace()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to determine namespace: %w", err)
|
||||
}
|
||||
if ns == "" {
|
||||
ns = "default"
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
// getRestConfig is a convenience function when only the rest.Config is needed
|
||||
// (used by buildRestConfig but also available for other callers).
|
||||
func getRestConfig() (*rest.Config, error) {
|
||||
if globalFlags.kubeconfig != "" || globalFlags.context != "" {
|
||||
return buildRestConfig()
|
||||
}
|
||||
config, err := ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
43
cmd/cozyctl/cmd/console.go
Normal file
43
cmd/cozyctl/cmd/console.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var consoleCmd = &cobra.Command{
|
||||
Use: "console <type> <name>",
|
||||
Short: "Open a serial console to a VirtualMachine",
|
||||
Long: `Open a serial console to a VirtualMachine using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runConsole,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(consoleCmd)
|
||||
}
|
||||
|
||||
func runConsole(cmd *cobra.Command, args []string) error {
|
||||
vmName, ns, err := resolveVMArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
virtctlArgs := []string{"virtctl", "console", vmName, "-n", ns}
|
||||
return execVirtctl(virtctlArgs)
|
||||
}
|
||||
112
cmd/cozyctl/cmd/discovery.go
Normal file
112
cmd/cozyctl/cmd/discovery.go
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// AppDefInfo holds resolved information about an ApplicationDefinition.
|
||||
type AppDefInfo struct {
|
||||
Name string // e.g. "postgres"
|
||||
Kind string // e.g. "Postgres"
|
||||
Plural string // e.g. "postgreses"
|
||||
Singular string // e.g. "postgres"
|
||||
Prefix string // e.g. "postgres-"
|
||||
IsModule bool
|
||||
}
|
||||
|
||||
// AppDefRegistry provides fast lookup of ApplicationDefinitions by plural, singular, or kind.
|
||||
type AppDefRegistry struct {
|
||||
byPlural map[string]*AppDefInfo
|
||||
bySingular map[string]*AppDefInfo
|
||||
byKind map[string]*AppDefInfo
|
||||
all []*AppDefInfo
|
||||
}
|
||||
|
||||
// discoverAppDefs lists all ApplicationDefinitions from the cluster and builds a registry.
|
||||
func discoverAppDefs(ctx context.Context, typedClient client.Client) (*AppDefRegistry, error) {
|
||||
var list cozyv1alpha1.ApplicationDefinitionList
|
||||
if err := typedClient.List(ctx, &list); err != nil {
|
||||
return nil, fmt.Errorf("failed to list ApplicationDefinitions: %w", err)
|
||||
}
|
||||
|
||||
reg := &AppDefRegistry{
|
||||
byPlural: make(map[string]*AppDefInfo),
|
||||
bySingular: make(map[string]*AppDefInfo),
|
||||
byKind: make(map[string]*AppDefInfo),
|
||||
}
|
||||
|
||||
for i := range list.Items {
|
||||
ad := &list.Items[i]
|
||||
info := &AppDefInfo{
|
||||
Name: ad.Name,
|
||||
Kind: ad.Spec.Application.Kind,
|
||||
Plural: ad.Spec.Application.Plural,
|
||||
Singular: ad.Spec.Application.Singular,
|
||||
Prefix: ad.Spec.Release.Prefix,
|
||||
IsModule: ad.Spec.Dashboard != nil && ad.Spec.Dashboard.Module,
|
||||
}
|
||||
reg.all = append(reg.all, info)
|
||||
reg.byPlural[strings.ToLower(info.Plural)] = info
|
||||
reg.bySingular[strings.ToLower(info.Singular)] = info
|
||||
reg.byKind[strings.ToLower(info.Kind)] = info
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// Resolve looks up an AppDefInfo by name (case-insensitive), checking plural, singular, then kind.
|
||||
func (r *AppDefRegistry) Resolve(name string) *AppDefInfo {
|
||||
lower := strings.ToLower(name)
|
||||
if info, ok := r.byPlural[lower]; ok {
|
||||
return info
|
||||
}
|
||||
if info, ok := r.bySingular[lower]; ok {
|
||||
return info
|
||||
}
|
||||
if info, ok := r.byKind[lower]; ok {
|
||||
return info
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveModule looks up an AppDefInfo among modules only.
|
||||
func (r *AppDefRegistry) ResolveModule(name string) *AppDefInfo {
|
||||
lower := strings.ToLower(name)
|
||||
for _, info := range r.all {
|
||||
if !info.IsModule {
|
||||
continue
|
||||
}
|
||||
if strings.ToLower(info.Plural) == lower ||
|
||||
strings.ToLower(info.Singular) == lower ||
|
||||
strings.ToLower(info.Kind) == lower {
|
||||
return info
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// All returns all discovered AppDefInfo entries.
|
||||
func (r *AppDefRegistry) All() []*AppDefInfo {
|
||||
return r.all
|
||||
}
|
||||
361
cmd/cozyctl/cmd/get.go
Normal file
361
cmd/cozyctl/cmd/get.go
Normal file
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
|
||||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
var getCmdFlags struct {
|
||||
target string
|
||||
}
|
||||
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get <type> [name]",
|
||||
Short: "Display one or many resources",
|
||||
Long: `Display one or many resources.
|
||||
|
||||
Built-in types:
|
||||
ns, namespaces Tenant namespaces (cluster-scoped)
|
||||
modules Tenant modules
|
||||
pvc, pvcs PersistentVolumeClaims
|
||||
|
||||
Sub-resource types (use -t to filter by parent application):
|
||||
secrets Secrets
|
||||
services, svc Services
|
||||
ingresses, ing Ingresses
|
||||
workloads WorkloadMonitors
|
||||
|
||||
Application types are discovered dynamically from ApplicationDefinitions.
|
||||
Use -t type/name to filter sub-resources by a specific application.`,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: runGet,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(getCmd)
|
||||
getCmd.Flags().StringVarP(&getCmdFlags.target, "target", "t", "", "Filter sub-resources by application type/name")
|
||||
}
|
||||
|
||||
func runGet(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
resourceType := args[0]
|
||||
var resourceName string
|
||||
if len(args) > 1 {
|
||||
resourceName = args[1]
|
||||
}
|
||||
|
||||
switch strings.ToLower(resourceType) {
|
||||
case "ns", "namespace", "namespaces":
|
||||
return getNamespaces(ctx, resourceName)
|
||||
case "module", "modules":
|
||||
return getModules(ctx, resourceName)
|
||||
case "pvc", "pvcs", "persistentvolumeclaim", "persistentvolumeclaims":
|
||||
return getPVCs(ctx, resourceName)
|
||||
case "secret", "secrets":
|
||||
return getSubResources(ctx, "secrets", resourceName)
|
||||
case "service", "services", "svc":
|
||||
return getSubResources(ctx, "services", resourceName)
|
||||
case "ingress", "ingresses", "ing":
|
||||
return getSubResources(ctx, "ingresses", resourceName)
|
||||
case "workload", "workloads":
|
||||
return getSubResources(ctx, "workloads", resourceName)
|
||||
default:
|
||||
return getApplications(ctx, resourceType, resourceName)
|
||||
}
|
||||
}
|
||||
|
||||
func getNamespaces(ctx context.Context, name string) error {
|
||||
_, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvr := schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}
|
||||
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get namespace %q: %w", name, err)
|
||||
}
|
||||
printNamespaces([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list namespaces: %w", err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, "namespaces")
|
||||
return nil
|
||||
}
|
||||
printNamespaces(list.Items)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getModules(ctx context.Context, name string) error {
|
||||
_, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvr := schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantmodules"}
|
||||
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get module %q: %w", name, err)
|
||||
}
|
||||
printModules([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list modules: %w", err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, "modules")
|
||||
return nil
|
||||
}
|
||||
printModules(list.Items)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPVCs(ctx context.Context, name string) error {
|
||||
_, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaims"}
|
||||
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get PVC %q: %w", name, err)
|
||||
}
|
||||
printPVCs([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list PVCs: %w", err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, "PVCs")
|
||||
return nil
|
||||
}
|
||||
printPVCs(list.Items)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSubResources(ctx context.Context, subType string, name string) error {
|
||||
typedClient, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
labelSelector, err := buildSubResourceSelector(ctx, typedClient, getCmdFlags.target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch subType {
|
||||
case "secrets":
|
||||
return getFilteredSecrets(ctx, dynClient, ns, name, labelSelector)
|
||||
case "services":
|
||||
return getFilteredServices(ctx, dynClient, ns, name, labelSelector)
|
||||
case "ingresses":
|
||||
return getFilteredIngresses(ctx, dynClient, ns, name, labelSelector)
|
||||
case "workloads":
|
||||
return getFilteredWorkloads(ctx, dynClient, ns, name, labelSelector)
|
||||
default:
|
||||
return fmt.Errorf("unknown sub-resource type: %s", subType)
|
||||
}
|
||||
}
|
||||
|
||||
func buildSubResourceSelector(ctx context.Context, typedClient client.Client, target string) (string, error) {
|
||||
var selectors []string
|
||||
|
||||
if target == "" {
|
||||
selectors = append(selectors, corev1alpha1.TenantResourceLabelKey+"="+corev1alpha1.TenantResourceLabelValue)
|
||||
return strings.Join(selectors, ","), nil
|
||||
}
|
||||
|
||||
parts := strings.SplitN(target, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid target format %q, expected type/name", target)
|
||||
}
|
||||
targetType, targetName := parts[0], parts[1]
|
||||
|
||||
// Discover ApplicationDefinitions to resolve the target type
|
||||
registry, err := discoverAppDefs(ctx, typedClient)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if this is a module reference
|
||||
if strings.ToLower(targetType) == "module" {
|
||||
info := registry.ResolveModule(targetName)
|
||||
if info == nil {
|
||||
return "", fmt.Errorf("unknown module %q", targetName)
|
||||
}
|
||||
selectors = append(selectors,
|
||||
appsv1alpha1.ApplicationKindLabel+"="+info.Kind,
|
||||
appsv1alpha1.ApplicationNameLabel+"="+targetName,
|
||||
corev1alpha1.TenantResourceLabelKey+"="+corev1alpha1.TenantResourceLabelValue,
|
||||
)
|
||||
return strings.Join(selectors, ","), nil
|
||||
}
|
||||
|
||||
info := registry.Resolve(targetType)
|
||||
if info == nil {
|
||||
return "", fmt.Errorf("unknown application type %q", targetType)
|
||||
}
|
||||
|
||||
selectors = append(selectors,
|
||||
appsv1alpha1.ApplicationKindLabel+"="+info.Kind,
|
||||
appsv1alpha1.ApplicationNameLabel+"="+targetName,
|
||||
corev1alpha1.TenantResourceLabelKey+"="+corev1alpha1.TenantResourceLabelValue,
|
||||
)
|
||||
return strings.Join(selectors, ","), nil
|
||||
}
|
||||
|
||||
func getFilteredSecrets(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
|
||||
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}
|
||||
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "secrets", printSecrets)
|
||||
}
|
||||
|
||||
func getFilteredServices(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
|
||||
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}
|
||||
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "services", printServices)
|
||||
}
|
||||
|
||||
func getFilteredIngresses(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
|
||||
gvr := schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}
|
||||
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "ingresses", printIngresses)
|
||||
}
|
||||
|
||||
func getFilteredWorkloads(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
|
||||
gvr := schema.GroupVersionResource{Group: "cozystack.io", Version: "v1alpha1", Resource: "workloadmonitors"}
|
||||
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "workloads", printWorkloads)
|
||||
}
|
||||
|
||||
func getFilteredResources(
|
||||
ctx context.Context,
|
||||
dynClient dynamic.Interface,
|
||||
gvr schema.GroupVersionResource,
|
||||
ns, name, labelSelector string,
|
||||
typeName string,
|
||||
printer func([]unstructured.Unstructured),
|
||||
) error {
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get %s %q: %w", typeName, name, err)
|
||||
}
|
||||
printer([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: labelSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list %s: %w", typeName, err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, typeName)
|
||||
return nil
|
||||
}
|
||||
printer(list.Items)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getApplications(ctx context.Context, resourceType, name string) error {
|
||||
typedClient, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registry, err := discoverAppDefs(ctx, typedClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info := registry.Resolve(resourceType)
|
||||
if info == nil {
|
||||
return fmt.Errorf("unknown resource type %q\nUse 'cozyctl get --help' for available types", resourceType)
|
||||
}
|
||||
|
||||
gvr := schema.GroupVersionResource{Group: "apps.cozystack.io", Version: "v1alpha1", Resource: info.Plural}
|
||||
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get %s %q: %w", info.Singular, name, err)
|
||||
}
|
||||
printApplications([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list %s: %w", info.Plural, err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, info.Plural)
|
||||
return nil
|
||||
}
|
||||
printApplications(list.Items)
|
||||
return nil
|
||||
}
|
||||
43
cmd/cozyctl/cmd/migrate.go
Normal file
43
cmd/cozyctl/cmd/migrate.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate <type> <name>",
|
||||
Short: "Live-migrate a VirtualMachine to another node",
|
||||
Long: `Live-migrate a VirtualMachine to another node using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runMigrate,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(migrateCmd)
|
||||
}
|
||||
|
||||
func runMigrate(cmd *cobra.Command, args []string) error {
|
||||
vmName, ns, err := resolveVMArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
virtctlArgs := []string{"virtctl", "migrate", vmName, "-n", ns}
|
||||
return execVirtctl(virtctlArgs)
|
||||
}
|
||||
51
cmd/cozyctl/cmd/portforward.go
Normal file
51
cmd/cozyctl/cmd/portforward.go
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var portForwardCmd = &cobra.Command{
|
||||
Use: "port-forward <type/name> [ports...]",
|
||||
Short: "Forward ports to a VirtualMachineInstance",
|
||||
Long: `Forward ports to a VirtualMachineInstance using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: runPortForward,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(portForwardCmd)
|
||||
}
|
||||
|
||||
func runPortForward(cmd *cobra.Command, args []string) error {
|
||||
vmName, ns, err := resolveVMArgs(args[:1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ports := args[1:]
|
||||
if len(ports) == 0 {
|
||||
return fmt.Errorf("at least one port is required")
|
||||
}
|
||||
|
||||
virtctlArgs := []string{"virtctl", "port-forward", "vmi/" + vmName, "-n", ns}
|
||||
virtctlArgs = append(virtctlArgs, ports...)
|
||||
return execVirtctl(virtctlArgs)
|
||||
}
|
||||
250
cmd/cozyctl/cmd/printer.go
Normal file
250
cmd/cozyctl/cmd/printer.go
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func newTabWriter() *tabwriter.Writer {
|
||||
return tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||
}
|
||||
|
||||
func printApplications(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tVERSION\tREADY\tSTATUS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
version, _, _ := unstructured.NestedString(item.Object, "appVersion")
|
||||
ready, status := extractCondition(item)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, version, ready, truncate(status, 48))
|
||||
}
|
||||
}
|
||||
|
||||
func printNamespaces(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME")
|
||||
for _, item := range items {
|
||||
fmt.Fprintln(w, item.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
func printModules(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tVERSION\tREADY\tSTATUS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
version, _, _ := unstructured.NestedString(item.Object, "appVersion")
|
||||
ready, status := extractCondition(item)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, version, ready, truncate(status, 48))
|
||||
}
|
||||
}
|
||||
|
||||
func printPVCs(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tSTATUS\tVOLUME\tCAPACITY\tSTORAGECLASS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
phase, _, _ := unstructured.NestedString(item.Object, "status", "phase")
|
||||
volume, _, _ := unstructured.NestedString(item.Object, "spec", "volumeName")
|
||||
capacity := ""
|
||||
if cap, ok, _ := unstructured.NestedStringMap(item.Object, "status", "capacity"); ok {
|
||||
capacity = cap["storage"]
|
||||
}
|
||||
sc, _, _ := unstructured.NestedString(item.Object, "spec", "storageClassName")
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", name, phase, volume, capacity, sc)
|
||||
}
|
||||
}
|
||||
|
||||
func printSecrets(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tTYPE\tDATA")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
secretType, _, _ := unstructured.NestedString(item.Object, "type")
|
||||
data, _, _ := unstructured.NestedMap(item.Object, "data")
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\n", name, secretType, len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func printServices(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tTYPE\tCLUSTER-IP\tEXTERNAL-IP\tPORTS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
svcType, _, _ := unstructured.NestedString(item.Object, "spec", "type")
|
||||
clusterIP, _, _ := unstructured.NestedString(item.Object, "spec", "clusterIP")
|
||||
|
||||
externalIP := "<none>"
|
||||
if lbIngress, ok, _ := unstructured.NestedSlice(item.Object, "status", "loadBalancer", "ingress"); ok && len(lbIngress) > 0 {
|
||||
var ips []string
|
||||
for _, ingress := range lbIngress {
|
||||
if m, ok := ingress.(map[string]interface{}); ok {
|
||||
if ip, ok := m["ip"].(string); ok {
|
||||
ips = append(ips, ip)
|
||||
} else if hostname, ok := m["hostname"].(string); ok {
|
||||
ips = append(ips, hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(ips) > 0 {
|
||||
externalIP = strings.Join(ips, ",")
|
||||
}
|
||||
}
|
||||
|
||||
ports := formatPorts(item)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", name, svcType, clusterIP, externalIP, ports)
|
||||
}
|
||||
}
|
||||
|
||||
func printIngresses(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tCLASS\tHOSTS\tADDRESS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
class, _, _ := unstructured.NestedString(item.Object, "spec", "ingressClassName")
|
||||
|
||||
var hosts []string
|
||||
if rules, ok, _ := unstructured.NestedSlice(item.Object, "spec", "rules"); ok {
|
||||
for _, rule := range rules {
|
||||
if m, ok := rule.(map[string]interface{}); ok {
|
||||
if host, ok := m["host"].(string); ok {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hostsStr := "<none>"
|
||||
if len(hosts) > 0 {
|
||||
hostsStr = strings.Join(hosts, ",")
|
||||
}
|
||||
|
||||
address := ""
|
||||
if lbIngress, ok, _ := unstructured.NestedSlice(item.Object, "status", "loadBalancer", "ingress"); ok && len(lbIngress) > 0 {
|
||||
var addrs []string
|
||||
for _, ingress := range lbIngress {
|
||||
if m, ok := ingress.(map[string]interface{}); ok {
|
||||
if ip, ok := m["ip"].(string); ok {
|
||||
addrs = append(addrs, ip)
|
||||
} else if hostname, ok := m["hostname"].(string); ok {
|
||||
addrs = append(addrs, hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
address = strings.Join(addrs, ",")
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, class, hostsStr, address)
|
||||
}
|
||||
}
|
||||
|
||||
func printWorkloads(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tKIND\tTYPE\tVERSION\tAVAILABLE\tOBSERVED\tOPERATIONAL")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
kind, _, _ := unstructured.NestedString(item.Object, "spec", "kind")
|
||||
wType, _, _ := unstructured.NestedString(item.Object, "spec", "type")
|
||||
version, _, _ := unstructured.NestedString(item.Object, "spec", "version")
|
||||
available, _, _ := unstructured.NestedInt64(item.Object, "status", "availableReplicas")
|
||||
observed, _, _ := unstructured.NestedInt64(item.Object, "status", "observedReplicas")
|
||||
operational, ok, _ := unstructured.NestedBool(item.Object, "status", "operational")
|
||||
opStr := ""
|
||||
if ok {
|
||||
opStr = fmt.Sprintf("%t", operational)
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%d\t%s\n", name, kind, wType, version, available, observed, opStr)
|
||||
}
|
||||
}
|
||||
|
||||
func printNoResources(w io.Writer, resourceType string) {
|
||||
fmt.Fprintf(w, "No %s found\n", resourceType)
|
||||
}
|
||||
|
||||
func extractCondition(item unstructured.Unstructured) (string, string) {
|
||||
conditions, ok, _ := unstructured.NestedSlice(item.Object, "status", "conditions")
|
||||
if !ok {
|
||||
return "Unknown", ""
|
||||
}
|
||||
for _, c := range conditions {
|
||||
cond, ok := c.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if cond["type"] == "Ready" {
|
||||
ready, _ := cond["status"].(string)
|
||||
message, _ := cond["message"].(string)
|
||||
return ready, message
|
||||
}
|
||||
}
|
||||
return "Unknown", ""
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
}
|
||||
|
||||
func formatPorts(item unstructured.Unstructured) string {
|
||||
ports, ok, _ := unstructured.NestedSlice(item.Object, "spec", "ports")
|
||||
if !ok || len(ports) == 0 {
|
||||
return "<none>"
|
||||
}
|
||||
var parts []string
|
||||
for _, p := range ports {
|
||||
port, ok := p.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
portNum, _, _ := unstructured.NestedInt64(port, "port")
|
||||
protocol, _, _ := unstructured.NestedString(port, "protocol")
|
||||
if protocol == "" {
|
||||
protocol = "TCP"
|
||||
}
|
||||
nodePort, _, _ := unstructured.NestedInt64(port, "nodePort")
|
||||
if nodePort > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d:%d/%s", portNum, nodePort, protocol))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("%d/%s", portNum, protocol))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
57
cmd/cozyctl/cmd/root.go
Normal file
57
cmd/cozyctl/cmd/root.go
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Version is set at build time via -ldflags.
|
||||
var Version = "dev"
|
||||
|
||||
var globalFlags struct {
|
||||
kubeconfig string
|
||||
context string
|
||||
namespace string
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "cozyctl",
|
||||
Short: "A CLI for managing Cozystack applications",
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
DisableAutoGenTag: true,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
func Execute() error {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Version = Version
|
||||
rootCmd.PersistentFlags().StringVar(&globalFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file")
|
||||
rootCmd.PersistentFlags().StringVar(&globalFlags.context, "context", "", "Kubernetes context to use")
|
||||
rootCmd.PersistentFlags().StringVarP(&globalFlags.namespace, "namespace", "n", "", "Kubernetes namespace")
|
||||
}
|
||||
106
cmd/cozyctl/cmd/vm.go
Normal file
106
cmd/cozyctl/cmd/vm.go
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// vmKindPrefix maps application Kind to the release prefix used by KubeVirt VMs.
|
||||
func vmKindPrefix(kind string) (string, bool) {
|
||||
switch kind {
|
||||
case "VirtualMachine":
|
||||
return "virtual-machine", true
|
||||
case "VMInstance":
|
||||
return "vm-instance", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// resolveVMArgs takes CLI args (type, name or type/name), resolves the application type
|
||||
// via discovery, validates it's a VM kind, and returns the full VM name and namespace.
|
||||
func resolveVMArgs(args []string) (string, string, error) {
|
||||
var resourceType, resourceName string
|
||||
|
||||
if len(args) == 1 {
|
||||
// type/name format
|
||||
parts := strings.SplitN(args[0], "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("expected type/name format, got %q", args[0])
|
||||
}
|
||||
resourceType, resourceName = parts[0], parts[1]
|
||||
} else {
|
||||
resourceType = args[0]
|
||||
resourceName = args[1]
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
typedClient, _, err := newClients()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
registry, err := discoverAppDefs(ctx, typedClient)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
info := registry.Resolve(resourceType)
|
||||
if info == nil {
|
||||
return "", "", fmt.Errorf("unknown application type %q", resourceType)
|
||||
}
|
||||
|
||||
prefix, ok := vmKindPrefix(info.Kind)
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("resource type %q (Kind=%s) is not a VirtualMachine or VMInstance", resourceType, info.Kind)
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
vmName := prefix + "-" + resourceName
|
||||
return vmName, ns, nil
|
||||
}
|
||||
|
||||
// execVirtctl replaces the current process with virtctl.
|
||||
func execVirtctl(args []string) error {
|
||||
virtctlPath, err := exec.LookPath("virtctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("virtctl not found in PATH: %w", err)
|
||||
}
|
||||
|
||||
// Append kubeconfig/context flags if set
|
||||
if globalFlags.kubeconfig != "" {
|
||||
args = append(args, "--kubeconfig", globalFlags.kubeconfig)
|
||||
}
|
||||
if globalFlags.context != "" {
|
||||
args = append(args, "--context", globalFlags.context)
|
||||
}
|
||||
|
||||
if err := syscall.Exec(virtctlPath, args, os.Environ()); err != nil {
|
||||
return fmt.Errorf("failed to exec virtctl: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
43
cmd/cozyctl/cmd/vnc.go
Normal file
43
cmd/cozyctl/cmd/vnc.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var vncCmd = &cobra.Command{
|
||||
Use: "vnc <type> <name>",
|
||||
Short: "Open a VNC connection to a VirtualMachine",
|
||||
Long: `Open a VNC connection to a VirtualMachine using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runVNC,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(vncCmd)
|
||||
}
|
||||
|
||||
func runVNC(cmd *cobra.Command, args []string) error {
|
||||
vmName, ns, err := resolveVMArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
virtctlArgs := []string{"virtctl", "vnc", vmName, "-n", ns}
|
||||
return execVirtctl(virtctlArgs)
|
||||
}
|
||||
29
cmd/cozyctl/main.go
Normal file
29
cmd/cozyctl/main.go
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
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 (
|
||||
"os"
|
||||
|
||||
"github.com/cozystack/cozystack/cmd/cozyctl/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user