diff --git a/pkg/apis/config/v1alpha1/kwokctl_component_types.go b/pkg/apis/config/v1alpha1/kwokctl_component_types.go new file mode 100644 index 0000000000..1597547ad5 --- /dev/null +++ b/pkg/apis/config/v1alpha1/kwokctl_component_types.go @@ -0,0 +1,43 @@ +/* +Copyright 2024 The Kubernetes 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 v1alpha1 + +import ( + "encoding/json" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // KwokctlComponentKind is the kind of the kwokctl component. + KwokctlComponentKind = "KwokctlComponent" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// KwokctlComponent holds information about the kwokctl component. +type KwokctlComponent struct { + //+k8s:conversion-gen=false + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta `json:"metadata,omitempty"` + // Parameters is the parameters for the kwokctl component configuration. + Parameters json.RawMessage `json:"parameters,omitempty"` + // Template is the template for the kwokctl component configuration. + Template string `json:"template,omitempty"` +} diff --git a/pkg/apis/config/v1alpha1/kwokctl_configuration_types.go b/pkg/apis/config/v1alpha1/kwokctl_configuration_types.go index 538e64f05b..acc78f61e4 100644 --- a/pkg/apis/config/v1alpha1/kwokctl_configuration_types.go +++ b/pkg/apis/config/v1alpha1/kwokctl_configuration_types.go @@ -505,6 +505,10 @@ type Component struct { // MetricsDiscovery is the metrics discovery of the component. MetricsDiscovery *ComponentMetric `json:"metricsDiscovery,omitempty"` + // Address is the address of the component. + // +optional + Address string `json:"address,omitempty"` + // Version is the version of the component. // +optional Version string `json:"version,omitempty"` diff --git a/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go index 1b5f881f11..77e7acf897 100644 --- a/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go @@ -239,6 +239,37 @@ func (in *KwokConfigurationOptions) DeepCopy() *KwokConfigurationOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KwokctlComponent) DeepCopyInto(out *KwokctlComponent) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KwokctlComponent. +func (in *KwokctlComponent) DeepCopy() *KwokctlComponent { + if in == nil { + return nil + } + out := new(KwokctlComponent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *KwokctlComponent) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KwokctlConfiguration) DeepCopyInto(out *KwokctlConfiguration) { *out = *in diff --git a/pkg/apis/internalversion/conversion.go b/pkg/apis/internalversion/conversion.go index 147c1ecd34..b2aeac28bc 100644 --- a/pkg/apis/internalversion/conversion.go +++ b/pkg/apis/internalversion/conversion.go @@ -68,6 +68,28 @@ func ConvertToInternalKwokctlResource(in *configv1alpha1.KwokctlResource) (*Kwok return &out, nil } +// ConvertToV1alpha1KwokctlComponent converts an internal version KwokctlComponent to a v1alpha1.KwokctlComponent. +func ConvertToV1alpha1KwokctlComponent(in *KwokctlComponent) (*configv1alpha1.KwokctlComponent, error) { + var out configv1alpha1.KwokctlComponent + out.APIVersion = configv1alpha1.GroupVersion.String() + out.Kind = configv1alpha1.KwokctlComponentKind + err := Convert_internalversion_KwokctlComponent_To_v1alpha1_KwokctlComponent(in, &out, nil) + if err != nil { + return nil, err + } + return &out, nil +} + +// ConvertToInternalKwokctlComponent converts a v1alpha1.KwokctlComponent to an internal version. +func ConvertToInternalKwokctlComponent(in *configv1alpha1.KwokctlComponent) (*KwokctlComponent, error) { + var out KwokctlComponent + err := Convert_v1alpha1_KwokctlComponent_To_internalversion_KwokctlComponent(in, &out, nil) + if err != nil { + return nil, err + } + return &out, nil +} + // ConvertToV1alpha1KwokConfiguration converts an internal version KwokConfiguration to a v1alpha1.KwokConfiguration. func ConvertToV1alpha1KwokConfiguration(in *KwokConfiguration) (*configv1alpha1.KwokConfiguration, error) { var out configv1alpha1.KwokConfiguration diff --git a/pkg/apis/internalversion/kwokctl_component_types.go b/pkg/apis/internalversion/kwokctl_component_types.go new file mode 100644 index 0000000000..3bc6849a4e --- /dev/null +++ b/pkg/apis/internalversion/kwokctl_component_types.go @@ -0,0 +1,34 @@ +/* +Copyright 2024 The Kubernetes 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 internalversion + +import ( + "encoding/json" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// KwokctlComponent provides component definition for kwokctl. +type KwokctlComponent struct { + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta + // Parameters is the parameters for the kwokctl component configuration. + Parameters json.RawMessage + // Template is the template for the kwokctl component configuration. + Template string +} diff --git a/pkg/apis/internalversion/kwokctl_configuration_types.go b/pkg/apis/internalversion/kwokctl_configuration_types.go index 43c875e250..28d3ff061d 100644 --- a/pkg/apis/internalversion/kwokctl_configuration_types.go +++ b/pkg/apis/internalversion/kwokctl_configuration_types.go @@ -338,6 +338,9 @@ type Component struct { // MetricsDiscovery is the metrics discovery of the component. MetricsDiscovery *ComponentMetric + // Address is the address of the component. + Address string + // Version is the version of the component. Version string } diff --git a/pkg/apis/internalversion/zz_generated.conversion.go b/pkg/apis/internalversion/zz_generated.conversion.go index e0b67e51d9..944c478a4a 100644 --- a/pkg/apis/internalversion/zz_generated.conversion.go +++ b/pkg/apis/internalversion/zz_generated.conversion.go @@ -340,6 +340,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*KwokctlComponent)(nil), (*configv1alpha1.KwokctlComponent)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_internalversion_KwokctlComponent_To_v1alpha1_KwokctlComponent(a.(*KwokctlComponent), b.(*configv1alpha1.KwokctlComponent), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*configv1alpha1.KwokctlComponent)(nil), (*KwokctlComponent)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_KwokctlComponent_To_internalversion_KwokctlComponent(a.(*configv1alpha1.KwokctlComponent), b.(*KwokctlComponent), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*KwokctlConfiguration)(nil), (*configv1alpha1.KwokctlConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_internalversion_KwokctlConfiguration_To_v1alpha1_KwokctlConfiguration(a.(*KwokctlConfiguration), b.(*configv1alpha1.KwokctlConfiguration), scope) }); err != nil { @@ -1071,6 +1081,7 @@ func autoConvert_internalversion_Component_To_v1alpha1_Component(in *Component, } out.Metric = (*configv1alpha1.ComponentMetric)(unsafe.Pointer(in.Metric)) out.MetricsDiscovery = (*configv1alpha1.ComponentMetric)(unsafe.Pointer(in.MetricsDiscovery)) + out.Address = in.Address out.Version = in.Version return nil } @@ -1104,6 +1115,7 @@ func autoConvert_v1alpha1_Component_To_internalversion_Component(in *configv1alp } out.Metric = (*ComponentMetric)(unsafe.Pointer(in.Metric)) out.MetricsDiscovery = (*ComponentMetric)(unsafe.Pointer(in.MetricsDiscovery)) + out.Address = in.Address out.Version = in.Version return nil } @@ -1564,6 +1576,31 @@ func Convert_v1alpha1_KwokConfigurationOptions_To_internalversion_KwokConfigurat return autoConvert_v1alpha1_KwokConfigurationOptions_To_internalversion_KwokConfigurationOptions(in, out, s) } +func autoConvert_internalversion_KwokctlComponent_To_v1alpha1_KwokctlComponent(in *KwokctlComponent, out *configv1alpha1.KwokctlComponent, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Parameters = *(*json.RawMessage)(unsafe.Pointer(&in.Parameters)) + out.Template = in.Template + return nil +} + +// Convert_internalversion_KwokctlComponent_To_v1alpha1_KwokctlComponent is an autogenerated conversion function. +func Convert_internalversion_KwokctlComponent_To_v1alpha1_KwokctlComponent(in *KwokctlComponent, out *configv1alpha1.KwokctlComponent, s conversion.Scope) error { + return autoConvert_internalversion_KwokctlComponent_To_v1alpha1_KwokctlComponent(in, out, s) +} + +func autoConvert_v1alpha1_KwokctlComponent_To_internalversion_KwokctlComponent(in *configv1alpha1.KwokctlComponent, out *KwokctlComponent, s conversion.Scope) error { + // INFO: in.TypeMeta opted out of conversion generation + out.ObjectMeta = in.ObjectMeta + out.Parameters = *(*json.RawMessage)(unsafe.Pointer(&in.Parameters)) + out.Template = in.Template + return nil +} + +// Convert_v1alpha1_KwokctlComponent_To_internalversion_KwokctlComponent is an autogenerated conversion function. +func Convert_v1alpha1_KwokctlComponent_To_internalversion_KwokctlComponent(in *configv1alpha1.KwokctlComponent, out *KwokctlComponent, s conversion.Scope) error { + return autoConvert_v1alpha1_KwokctlComponent_To_internalversion_KwokctlComponent(in, out, s) +} + func autoConvert_internalversion_KwokctlConfiguration_To_v1alpha1_KwokctlConfiguration(in *KwokctlConfiguration, out *configv1alpha1.KwokctlConfiguration, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_internalversion_KwokctlConfigurationOptions_To_v1alpha1_KwokctlConfigurationOptions(&in.Options, &out.Options, s); err != nil { diff --git a/pkg/apis/internalversion/zz_generated.deepcopy.go b/pkg/apis/internalversion/zz_generated.deepcopy.go index 3bbf08b6a7..a80ebc600e 100644 --- a/pkg/apis/internalversion/zz_generated.deepcopy.go +++ b/pkg/apis/internalversion/zz_generated.deepcopy.go @@ -695,6 +695,28 @@ func (in *KwokConfigurationOptions) DeepCopy() *KwokConfigurationOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KwokctlComponent) DeepCopyInto(out *KwokctlComponent) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KwokctlComponent. +func (in *KwokctlComponent) DeepCopy() *KwokctlComponent { + if in == nil { + return nil + } + out := new(KwokctlComponent) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KwokctlConfiguration) DeepCopyInto(out *KwokctlConfiguration) { *out = *in diff --git a/pkg/config/config.go b/pkg/config/config.go index 02de27a194..0f6d654662 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -107,6 +107,12 @@ var configHandlers = map[string]configHandler{ MutateToInternal: mutateToInternalConfig(internalversion.ConvertToInternalKwokctlResource), MutateToVersiond: mutateToVersiondConfig(internalversion.ConvertToV1alpha1KwokctlResource), }, + configv1alpha1.KwokctlComponentKind: { + Unmarshal: unmarshalConfig[*configv1alpha1.KwokctlComponent], + Marshal: marshalConfig, + MutateToInternal: mutateToInternalConfig(internalversion.ConvertToInternalKwokctlComponent), + MutateToVersiond: mutateToVersiondConfig(internalversion.ConvertToV1alpha1KwokctlComponent), + }, v1alpha1.StageKind: { Unmarshal: unmarshalConfig[*v1alpha1.Stage], Marshal: marshalConfig, diff --git a/pkg/kwokctl/cmd/create/cluster/cluster.go b/pkg/kwokctl/cmd/create/cluster/cluster.go index 24e8774b23..fd4dd5d8b8 100644 --- a/pkg/kwokctl/cmd/create/cluster/cluster.go +++ b/pkg/kwokctl/cmd/create/cluster/cluster.go @@ -33,6 +33,7 @@ import ( "sigs.k8s.io/kwok/pkg/log" "sigs.k8s.io/kwok/pkg/utils/kubeconfig" "sigs.k8s.io/kwok/pkg/utils/path" + "sigs.k8s.io/kwok/pkg/utils/slices" ) type flagpole struct { @@ -42,6 +43,9 @@ type flagpole struct { Kubeconfig string ExtraArgs []string + ComponentArgs []string + EnableComponents []string + *internalversion.KwokctlConfiguration } @@ -145,6 +149,9 @@ func NewCommand(ctx context.Context) *cobra.Command { cmd.Flags().Float64Var(&flags.Options.HeartbeatFactor, "heartbeat-factor", flags.Options.HeartbeatFactor, "Scale factor for all about heartbeat") cmd.Flags().StringArrayVar(&flags.ExtraArgs, "extra-args", flags.ExtraArgs, "Pass a single extra arg key-value pair to the component in the format `component=key=value`") + cmd.Flags().StringArrayVar(&flags.ComponentArgs, "component-args", flags.ComponentArgs, "Set component args for the cluster, format: .key1=value1 .key2=value2") + cmd.Flags().StringArrayVar(&flags.EnableComponents, "enable-components", flags.EnableComponents, "Enable components for the cluster") + return cmd } @@ -285,6 +292,24 @@ func runE(ctx context.Context, flags *flagpole) error { cleanUp() return err } + + // Enable components + for _, componentName := range flags.EnableComponents { + componentPrefix := fmt.Sprintf(".%s.", componentName) + args := slices.FilterAndMap(flags.ComponentArgs, func(s string) (string, bool) { + if !strings.HasPrefix(s, componentPrefix) { + return "", false + } + return s[len(componentPrefix)-1:], true + }) + err := rt.SetComponents(ctx, componentName, args...) + if err != nil { + logger.Error("Failed to set components", err, "component", componentName) + cleanUp() + return err + } + } + err = rt.Save(ctx) if err != nil { logger.Error("Failed to save config", err) diff --git a/pkg/kwokctl/components/utils.go b/pkg/kwokctl/components/utils.go index 92955d5562..3d23d486e5 100644 --- a/pkg/kwokctl/components/utils.go +++ b/pkg/kwokctl/components/utils.go @@ -94,3 +94,29 @@ var ( func GetRuntimeMode(runtime string) string { return runtimeTypeMap[runtime] } + +// PatchComponent patches a component. +func PatchComponent(c internalversion.Component, patch internalversion.ComponentPatches) internalversion.Component { + for _, a := range patch.ExtraArgs { + c.Args = append(c.Args, fmt.Sprintf("--%s=%s", a.Key, a.Value)) + } + + for _, v := range patch.ExtraVolumes { + c.Volumes = append(c.Volumes, internalversion.Volume{ + Name: v.Name, + HostPath: v.HostPath, + PathType: v.PathType, + MountPath: v.MountPath, + ReadOnly: v.ReadOnly, + }) + } + + for _, e := range patch.ExtraEnvs { + c.Envs = append(c.Envs, internalversion.Env{ + Name: e.Name, + Value: e.Value, + }) + } + + return c +} diff --git a/pkg/kwokctl/runtime/binary/cluster.go b/pkg/kwokctl/runtime/binary/cluster.go index 8ec2556366..c948c9f541 100644 --- a/pkg/kwokctl/runtime/binary/cluster.go +++ b/pkg/kwokctl/runtime/binary/cluster.go @@ -822,8 +822,12 @@ func (c *Cluster) startComponent(ctx context.Context, component internalversion. ctx = exec.WithUser(ctx, &uid, &gid) } + workdir := component.WorkDir + if workdir == "" { + workdir = c.Workdir() + } logger.Debug("Starting component") - return c.ForkExec(ctx, component.WorkDir, component.Binary, component.Args...) + return c.ForkExec(ctx, workdir, component.Binary, component.Args...) } func (c *Cluster) startComponents(ctx context.Context) error { diff --git a/pkg/kwokctl/runtime/binary/component.go b/pkg/kwokctl/runtime/binary/component.go new file mode 100644 index 0000000000..eecd4e3a16 --- /dev/null +++ b/pkg/kwokctl/runtime/binary/component.go @@ -0,0 +1,103 @@ +/* +Copyright 2024 The Kubernetes 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 binary + +import ( + "context" + "encoding/json" + "fmt" + + "sigs.k8s.io/kwok/pkg/apis/internalversion" + "sigs.k8s.io/kwok/pkg/config" + "sigs.k8s.io/kwok/pkg/kwokctl/runtime" + "sigs.k8s.io/kwok/pkg/kwokctl/scale" + "sigs.k8s.io/kwok/pkg/utils/gotpl" + "sigs.k8s.io/kwok/pkg/utils/path" + "sigs.k8s.io/kwok/pkg/utils/slices" +) + +// SetComponents returns the components of cluster +func (c *Cluster) SetComponents(ctx context.Context, name string, args ...string) error { + conf, err := c.Config(ctx) + if err != nil { + return err + } + _, ok := slices.Find(conf.Components, func(component internalversion.Component) bool { + return component.Name == name + }) + if ok { + return fmt.Errorf("component %s is already exists", name) + } + + kcp := config.FilterWithTypeFromContext[*internalversion.KwokctlComponent](ctx) + renderer := gotpl.NewRenderer(gotpl.FuncMap{ + "ClusterName": c.Name, + "Workdir": c.Workdir, + "Runtime": func() string { + return c.Runtime(ctx) + }, + "Mode": func() string { + return c.Mode(ctx) + }, + "Address": func() string { + return c.ComponentAddress(ctx, name) + }, + "PkiDir": func() string { + return path.Join(c.Workdir(), runtime.PkiName) + }, + "Kubeconfig": func() string { + return c.GetWorkdirPath(runtime.InHostKubeconfigName) + }, + "Config": func() *internalversion.KwokctlConfiguration { + return conf + }, + }) + + krc, ok := slices.Find(kcp, func(krc *internalversion.KwokctlComponent) bool { + return krc.Name == name + }) + if !ok { + return fmt.Errorf("component %s is not exists", name) + } + + param, err := scale.NewParameters(ctx, krc.Parameters, args) + if err != nil { + return err + } + + componentData, err := renderer.ToJSON(krc.Template, param) + if err != nil { + return err + } + var component internalversion.Component + err = json.Unmarshal(componentData, &component) + if err != nil { + return err + } + component.Name = name + + binaryPath, err := c.EnsureBinary(ctx, component.Name, component.Binary) + if err != nil { + return err + } + + component.Binary = binaryPath + + conf.Components = append(conf.Components, component) + + return c.SetConfig(ctx, conf) +} diff --git a/pkg/kwokctl/runtime/component.go b/pkg/kwokctl/runtime/component.go new file mode 100644 index 0000000000..912003ef2e --- /dev/null +++ b/pkg/kwokctl/runtime/component.go @@ -0,0 +1,47 @@ +/* +Copyright 2024 The Kubernetes 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 runtime + +import ( + "context" + + "sigs.k8s.io/kwok/pkg/kwokctl/components" + "sigs.k8s.io/kwok/pkg/utils/net" +) + +func (c *Cluster) Runtime(ctx context.Context) string { + config, err := c.Config(ctx) + if err != nil { + return "" + } + conf := &config.Options + + return conf.Runtime +} + +func (c *Cluster) Mode(ctx context.Context) string { + return components.GetRuntimeMode(c.Runtime(ctx)) +} + +func (c *Cluster) ComponentAddress(ctx context.Context, name string) string { + switch c.Mode(ctx) { + case components.RuntimeModeContainer: + return c.Name() + "-" + name + default: + return net.LocalAddress + } +} diff --git a/pkg/kwokctl/runtime/compose/component.go b/pkg/kwokctl/runtime/compose/component.go new file mode 100644 index 0000000000..f6a8d40bd3 --- /dev/null +++ b/pkg/kwokctl/runtime/compose/component.go @@ -0,0 +1,121 @@ +/* +Copyright 2024 The Kubernetes 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 compose + +import ( + "context" + "encoding/json" + "fmt" + + "sigs.k8s.io/kwok/pkg/apis/internalversion" + "sigs.k8s.io/kwok/pkg/config" + "sigs.k8s.io/kwok/pkg/consts" + "sigs.k8s.io/kwok/pkg/kwokctl/runtime" + "sigs.k8s.io/kwok/pkg/kwokctl/scale" + "sigs.k8s.io/kwok/pkg/utils/gotpl" + "sigs.k8s.io/kwok/pkg/utils/slices" +) + +// SetComponents returns the components of cluster +func (c *Cluster) SetComponents(ctx context.Context, name string, args ...string) error { + conf, err := c.Config(ctx) + if err != nil { + return err + } + _, ok := slices.Find(conf.Components, func(component internalversion.Component) bool { + return component.Name == name + }) + if ok { + return fmt.Errorf("component %s is already exists", name) + } + + kcp := config.FilterWithTypeFromContext[*internalversion.KwokctlComponent](ctx) + renderer := gotpl.NewRenderer(gotpl.FuncMap{ + "ClusterName": c.Name, + "Workdir": c.Workdir, + "Runtime": func() string { + return c.Runtime(ctx) + }, + "Mode": func() string { + return c.Mode(ctx) + }, + "Address": func() string { + return c.ComponentAddress(ctx, name) + }, + "PkiDir": func() string { + return "/etc/kubernetes/pki" + }, + "Kubeconfig": func() string { + return "/root/.kube/config" + }, + "Config": func() *internalversion.KwokctlConfiguration { + return conf + }, + }) + + krc, ok := slices.Find(kcp, func(krc *internalversion.KwokctlComponent) bool { + return krc.Name == name + }) + if !ok { + return fmt.Errorf("component %s is not exists", name) + } + + param, err := scale.NewParameters(ctx, krc.Parameters, args) + if err != nil { + return err + } + + componentData, err := renderer.ToJSON(krc.Template, param) + if err != nil { + return err + } + var component internalversion.Component + err = json.Unmarshal(componentData, &component) + if err != nil { + return err + } + component.Name = name + + volumes := []internalversion.Volume{ + { + HostPath: c.GetWorkdirPath(runtime.InClusterKubeconfigName), + MountPath: "/root/.kube/config", + }, + { + HostPath: c.GetWorkdirPath(runtime.PkiName), + MountPath: "/etc/kubernetes/pki/", + }, + } + + if name == consts.ComponentKwokController { + volumes = append(volumes, internalversion.Volume{ + HostPath: c.GetWorkdirPath(runtime.ConfigName), + MountPath: "/root/.kwok/kwok.yaml", + }) + } + + component.Volumes = append(component.Volumes, volumes...) + + err = c.EnsureImage(ctx, c.runtime, component.Image) + if err != nil { + return err + } + + conf.Components = append(conf.Components, component) + + return c.SetConfig(ctx, conf) +} diff --git a/pkg/kwokctl/runtime/config.go b/pkg/kwokctl/runtime/config.go index d8d0141c1c..9d9f4675bd 100644 --- a/pkg/kwokctl/runtime/config.go +++ b/pkg/kwokctl/runtime/config.go @@ -70,6 +70,9 @@ type Runtime interface { // ListComponents list the components of cluster ListComponents(ctx context.Context) ([]internalversion.Component, error) + // SetComponents returns the components of cluster + SetComponents(ctx context.Context, name string, args ...string) error + // InspectComponent inspect the component InspectComponent(ctx context.Context, name string) (ComponentStatus, error) diff --git a/pkg/kwokctl/runtime/kind/component.go b/pkg/kwokctl/runtime/kind/component.go new file mode 100644 index 0000000000..245eaa554f --- /dev/null +++ b/pkg/kwokctl/runtime/kind/component.go @@ -0,0 +1,99 @@ +/* +Copyright 2024 The Kubernetes 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 kind + +import ( + "context" + "encoding/json" + "fmt" + + "sigs.k8s.io/kwok/pkg/apis/internalversion" + "sigs.k8s.io/kwok/pkg/config" + "sigs.k8s.io/kwok/pkg/kwokctl/scale" + "sigs.k8s.io/kwok/pkg/utils/gotpl" + "sigs.k8s.io/kwok/pkg/utils/slices" +) + +// SetComponents returns the components of cluster +func (c *Cluster) SetComponents(ctx context.Context, name string, args ...string) error { + conf, err := c.Config(ctx) + if err != nil { + return err + } + _, ok := slices.Find(conf.Components, func(component internalversion.Component) bool { + return component.Name == name + }) + if ok { + return fmt.Errorf("component %s is already exists", name) + } + + kcp := config.FilterWithTypeFromContext[*internalversion.KwokctlComponent](ctx) + renderer := gotpl.NewRenderer(gotpl.FuncMap{ + "ClusterName": c.Name, + "Workdir": c.Workdir, + "Runtime": func() string { + return c.Runtime(ctx) + }, + "Mode": func() string { + return c.Mode(ctx) + }, + "Address": func() string { + return c.ComponentAddress(ctx, name) + }, + "PkiDir": func() string { + return "/etc/kubernetes/pki" + }, + "Kubeconfig": func() string { + return "/root/.kube/config" + }, + "Config": func() *internalversion.KwokctlConfiguration { + return conf + }, + }) + + krc, ok := slices.Find(kcp, func(krc *internalversion.KwokctlComponent) bool { + return krc.Name == name + }) + if !ok { + return fmt.Errorf("component %s is not exists", name) + } + + param, err := scale.NewParameters(ctx, krc.Parameters, args) + if err != nil { + return err + } + + componentData, err := renderer.ToJSON(krc.Template, param) + if err != nil { + return err + } + var component internalversion.Component + err = json.Unmarshal(componentData, &component) + if err != nil { + return err + } + component.Name = name + + err = c.EnsureImage(ctx, c.runtime, component.Image) + if err != nil { + return err + } + + conf.Components = append(conf.Components, component) + + return c.SetConfig(ctx, conf) +} diff --git a/pkg/utils/gotpl/funcs.go b/pkg/utils/gotpl/funcs.go index 40c2c08105..d4e3435e23 100644 --- a/pkg/utils/gotpl/funcs.go +++ b/pkg/utils/gotpl/funcs.go @@ -19,6 +19,7 @@ package gotpl import ( "encoding/json" "fmt" + "runtime" "strconv" "strings" "time" @@ -36,6 +37,15 @@ var ( genericFuncs = sprig.TxtFuncMap() ) +func init() { + genericFuncs["GOOS"] = func() string { + return runtime.GOOS + } + genericFuncs["GOARCH"] = func() string { + return runtime.GOARCH + } +} + var ( startTime = time.Now().Format(time.RFC3339Nano) diff --git a/pkg/utils/gotpl/renderer.go b/pkg/utils/gotpl/renderer.go index 26996a9573..fe794dc868 100644 --- a/pkg/utils/gotpl/renderer.go +++ b/pkg/utils/gotpl/renderer.go @@ -101,7 +101,7 @@ func (r *renderer) ToText(text string, original interface{}) ([]byte, error) { err := r.render(buf, text, original) if err != nil { - return nil, fmt.Errorf("%w: %s", err, buf.String()) + return nil, fmt.Errorf("%w: %s", err, strings.TrimRight(buf.String(), "\x00")) } return slices.Clone(buf.Bytes()), nil } @@ -113,12 +113,12 @@ func (r *renderer) ToJSON(text string, original interface{}) ([]byte, error) { err := r.render(buf, text, original) if err != nil { - return nil, fmt.Errorf("%w: %s", err, buf.String()) + return nil, fmt.Errorf("%w: %s", err, strings.TrimRight(buf.String(), "\x00")) } out, err := yaml.YAMLToJSON(buf.Bytes()) if err != nil { - return nil, fmt.Errorf("%w: %s", err, buf.String()) + return nil, fmt.Errorf("%w: %s", err, strings.TrimRight(buf.String(), "\x00")) } return out, nil } diff --git a/pkg/utils/slices/slices.go b/pkg/utils/slices/slices.go index 65be179d50..b0dd356a90 100644 --- a/pkg/utils/slices/slices.go +++ b/pkg/utils/slices/slices.go @@ -16,6 +16,11 @@ limitations under the License. package slices +import ( + "cmp" + "sort" +) + // Map returns a new slice containing the results of applying the given function func Map[S ~[]T, T any, O any](s S, f func(T) O) []O { out := make([]O, len(s)) @@ -129,3 +134,13 @@ func GroupBy[S ~[]T, T any, K comparable](s S, f func(T) K) map[K][]T { } return out } + +// Sort returns a new slice containing the elements of the slice in sorted order. +func Sort[S ~[]T, T cmp.Ordered](s S) []T { + out := make([]T, len(s)) + copy(out, s) + sort.Slice(out, func(i, j int) bool { + return cmp.Less(out[i], out[j]) + }) + return out +} diff --git a/site/content/en/docs/generated/apis.md b/site/content/en/docs/generated/apis.md index aa8b30e5a2..e7d599dddd 100644 --- a/site/content/en/docs/generated/apis.md +++ b/site/content/en/docs/generated/apis.md @@ -137,6 +137,9 @@ Resource Types: KwokConfiguration
  • +KwokctlComponent +
  • +
  • KwokctlConfiguration
  • @@ -206,6 +209,79 @@ KwokConfigurationOptions +

    +KwokctlComponent + # +

    +

    +

    KwokctlComponent holds information about the kwokctl component.

    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +apiVersion +string + + +config.kwok.x-k8s.io/v1alpha1 + +
    +kind +string +KwokctlComponent
    +metadata + + +Kubernetes meta/v1.ObjectMeta + + + +

    Standard list metadata. +More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

    +Refer to the Kubernetes API documentation for the fields of the +metadata field. +
    +parameters + +encoding/json.RawMessage + + +

    Parameters is the parameters for the kwokctl component configuration.

    +
    +template + +string + + +

    Template is the template for the kwokctl component configuration.

    +

    KwokctlConfiguration # @@ -2007,6 +2083,18 @@ ComponentMetric +address + +string + + + +(Optional) +

    Address is the address of the component.

    + + + + version string diff --git a/site/content/en/docs/generated/kwokctl_create_cluster.md b/site/content/en/docs/generated/kwokctl_create_cluster.md index 476049e74d..331ba96b62 100644 --- a/site/content/en/docs/generated/kwokctl_create_cluster.md +++ b/site/content/en/docs/generated/kwokctl_create_cluster.md @@ -9,6 +9,7 @@ kwokctl create cluster [flags] ### Options ``` + --component-args stringArray Set component args for the cluster, format: .key1=value1 .key2=value2 --controller-port uint32 Port of kwok-controller given to the host --dashboard-image string Image of dashboard, only for docker/podman/nerdctl/kind/kind-podman runtime '${KWOK_DASHBOARD_IMAGE_PREFIX}/dashboard:${KWOK_DASHBOARD_VERSION}' @@ -17,6 +18,7 @@ kwokctl create cluster [flags] --disable-kube-controller-manager Disable the kube-controller-manager --disable-kube-scheduler Disable the kube-scheduler --disable-qps-limits Disable QPS limits for components + --enable-components stringArray Enable components for the cluster --enable-crds strings List of CRDs to enable --enable-metrics-server Enable the metrics-server --etcd-binary string Binary of etcd, only for binary runtime (default "https://github.com/etcd-io/etcd/releases/download/v3.5.11/etcd-v3.5.11-linux-amd64.tar.gz#etcd")