diff --git a/.github/workflows/integration-per-language.yml b/.github/workflows/integration-per-language.yml index 474d875c4..3134d66a8 100644 --- a/.github/workflows/integration-per-language.yml +++ b/.github/workflows/integration-per-language.yml @@ -67,7 +67,7 @@ jobs: env: imagename: registry:5001/testapp serviceport: 80 - ingress_test_args: "-a webapp_routing --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1" + ingress_test_args: "-a app-routing-ingress --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -244,7 +244,7 @@ jobs: env: imagename: registry:5001/testapp serviceport: 80 - ingress_test_args: "-a webapp_routing --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1" + ingress_test_args: "-a app-routing-ingress --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -415,7 +415,7 @@ jobs: env: imagename: registry:5001/testapp serviceport: 80 - ingress_test_args: "-a webapp_routing --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1" + ingress_test_args: "-a app-routing-ingress --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -521,7 +521,7 @@ jobs: needs: manifests-create runs-on: ubuntu-latest env: - ingress_test_args: "-a webapp_routing --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1" + ingress_test_args: "-a app-routing-ingress --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1" services: registry: image: registry:2 @@ -611,7 +611,7 @@ jobs: name: ${{inputs.language}}-win-helm-create path: ./langtest/ - run: Remove-Item ./langtest/charts/templates/ingress.yaml -Recurse -Force -ErrorAction Ignore - - run: ./draft.exe -v update -d ./langtest/ -a webapp_routing --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1 + - run: ./draft.exe -v update -d ./langtest/ -a app-routing-ingress --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: check_windows_addon_helm @@ -661,7 +661,7 @@ jobs: name: ${{inputs.language}}-win-kustomize-create path: ./langtest - run: Remove-Item ./langtest/overlays/production/ingress.yaml -ErrorAction Ignore - - run: ./draft.exe -v update -d ./langtest/ -a webapp_routing --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1 + - run: ./draft.exe -v update -d ./langtest/ -a app-routing-ingress --variable ingress-tls-cert-keyvault-uri=test.cert.keyvault.uri --variable ingress-use-osm-mtls=true --variable ingress-host=host1 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: check_windows_addon_kustomize diff --git a/README.md b/README.md index 1027abdba..b85752cb1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ![Draft logo](./ghAssets/Draft-Gradient@3x.png) +

@@ -21,6 +22,7 @@
## Installation + ### Homebrew 1. Run the following commands @@ -43,7 +45,7 @@ Draft is a tool made for users who are just getting started with Kubernetes or w ### `draft create` -In our directory that holds our application, we can run the CLI command ‘draft create’. Draft create will walk you through a series of questions prompting you on your application specification. At the end of it, you will have a Dockerfile as well as Kubernetes manifests to deploy your application. Below is a picture of running the Draft create command on our [Contoso Air repository](https://github.com/microsoft/ContosoAir). +In our directory that holds our application, we can run the CLI command ‘draft create’. Draft create will walk you through a series of questions prompting you on your application specification. At the end of it, you will have a Dockerfile as well as Kubernetes manifests to deploy your application. Below is a picture of running the Draft create command on our [Contoso Air repository](https://github.com/Azure-Samples/contoso-air). ![example of draft create command showing the prompt "select k8s deployment type" with three options "helm", "kustomize", and "manifests"](./ghAssets/draft-create.png) @@ -71,11 +73,26 @@ Draft validate scans your manifests and populates warnings messages in your code ![screenshot of draft-validate](./ghAssets/draft-validate.png) +### `draft distribute` + +The `draft distribute` command generates [KubeFleet](https://kubefleet.dev) ClusterResourcePlacement manifests to distribute your application resources across multiple Kubernetes clusters managed by KubeFleet. This command is specifically designed for multi-cluster resource placement scenarios and requires existing files created using `draft create`. [Read the documentation for this command](docs/kubefleet-clusterresourceplacement.md) for more details. + +Example usage: + +```bash +draft distribute --variable CRP_NAME=demo-crp \ + --variable RESOURCE_SELECTOR_NAME=demo-namespace \ + --variable PLACEMENT_TYPE=PickAll \ + --variable PARTOF=my-project +``` + ### `draft info` + The `draft info` command prints information about supported languages and deployment types. Example output (for brevity, only the first supported language is shown): -``` + +```json { "supportedLanguages": [ { @@ -97,6 +114,7 @@ Example output (for brevity, only the first supported language is shown): ] } ``` + ## About The Project @@ -105,20 +123,23 @@ Draft makes it easier for developers to get started building apps that run on Ku ### Commands -- `draft create` adds the minimum required Dockerfile and manifest files for your deployment to the project directory. - - Supported deployment types: Helm, Kustomize, Kubernetes manifest. -- `draft setup-gh` automates the GitHub OIDC setup process for your project. -- `draft generate-workflow` generates a GitHub Actions workflow for automatic build and deploy to a Kubernetes cluster. -- `draft update` automatically make your application to be internet accessible. -- `draft validate` scan your manifests to see if they are following Kubernetes best practices. -- `draft info` print supported language and field information in json format. +* `draft create` adds the minimum required Dockerfile and manifest files for your deployment to the project directory. + * Supported deployment types: Helm, Kustomize, Kubernetes manifest. +* `draft setup-gh` automates the GitHub OIDC setup process for your project. +* `draft generate-workflow` generates a GitHub Actions workflow for automatic build and deploy to a Kubernetes cluster. +* `draft update` automatically make your application to be internet accessible. +* `draft distribute` distributes your application resources across Kubernetes clusters using [KubeFleet](https://kubefleet.dev/) ClusterResourcePlacement manifests. +* `draft validate` scan your manifests to see if they are following Kubernetes best practices. +* `draft info` print supported language and field information in json format. Use `draft [command] --help` for more information about a command. ### Dry Run + The following flags can be used for enabling dry running, which is currently supported by the following commands: `create` -- ` --dry-run` enables dry run mode in which no files are written to disk -- `--dry-run-file` specifies a file to write the dry run summary in json format into + +* `--dry-run` enables dry run mode in which no files are written to disk +* `--dry-run-file` specifies a file to write the dry run summary in json format into ```json // Example dry run output @@ -146,6 +167,7 @@ The following flags can be used for enabling dry running, which is currently sup ] } ``` + ## Install from Source ### Prerequisites @@ -177,12 +199,12 @@ go version mv draft $HOME/go/bin/ ``` - ## Draft as a Dependency If you are looking to leverage Draft's file generation capabilities and templating within another project instead of using the CLI, you have two options: importing the Draft go packages, and wrapping the binary ### Importing Draft Go Packages + This option will provide the cleanest integration, as it directly builds Draft into your project. However, it requires that your project is written in Go. Dockerfiles can be generated following the example in [examples/dockerfile.go](https://github.com/Azure/draft/blob/main/example/dockerfile.go) @@ -190,13 +212,15 @@ Dockerfiles can be generated following the example in [examples/dockerfile.go](h Deployment files can be generated following the example in [examples/deployment.go](https://github.com/Azure/draft/blob/main/example/deployment.go) ### Wrapping the Binary + For projects written in languages other than Go, or for projects that prefer to not import the packages directly, you can wrap the Draft binary. Several features have been implemented to make consuming draft as easy as possible: -- `draft info` prints supported language and field information in json format for easy parsing -- `--dry-run` and `--dry-run-file` flags can be used on the `create` and `update` commands to generate a summary of the files that would be written to disk, and the variables that would be used in the templates -- `draft update` and `draft create` accept a repeatable `--variable` flag that can be used to set template variables -- `draft create` takes a `--create-config` flag that can be used to input variables through a yaml file instead of interactively + +* `draft info` prints supported language and field information in json format for easy parsing +* `--dry-run` and `--dry-run-file` flags can be used on the `create`, `update` and `ditribute` commands to generate a summary of the files that would be written to disk, and the variables that would be used in the templates +* `draft update`, `draft create` and `draft distribute` accept a repeatable `--variable` flag that can be used to set template variables +* `draft create` takes a `--create-config` flag that can be used to input variables through a yaml file instead of interactively ## Introduction Videos diff --git a/cmd/distribute.go b/cmd/distribute.go new file mode 100644 index 000000000..58882ada3 --- /dev/null +++ b/cmd/distribute.go @@ -0,0 +1,131 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/Azure/draft/pkg/cmdhelpers" + "github.com/Azure/draft/pkg/config" + dryrunpkg "github.com/Azure/draft/pkg/dryrun" + "github.com/Azure/draft/pkg/handlers" + "github.com/Azure/draft/pkg/templatewriter" + "github.com/Azure/draft/pkg/templatewriter/writers" +) + +type distributeCmd struct { + dest string + provider string + addon string + flagVariables []string + templateWriter templatewriter.TemplateWriter + templateVariableRecorder config.TemplateVariableRecorder +} + +var distributeDryRunRecorder *dryrunpkg.DryRunRecorder + +func newDistributeCmd() *cobra.Command { + dc := &distributeCmd{} + // distributeCmd represents the distribute command + var cmd = &cobra.Command{ + Use: "distribute", + Short: "Distributes your application resources across Kubernetes clusters using Kubefleet", + Long: `This command generates Kubefleet ClusterResourcePlacement manifests to distribute your application + resources across multiple Kubernetes clusters managed by Kubefleet.`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := dc.run(); err != nil { + return err + } + log.Info("Draft has successfully created your Kubefleet ClusterResourcePlacement manifest for resource distribution 😃") + return nil + }, + } + f := cmd.Flags() + f.StringVarP(&dc.dest, "destination", "d", ".", "specify the path to the project directory") + f.StringVarP(&dc.provider, "provider", "p", "azure", "cloud provider") + f.StringVarP(&dc.addon, "addon", "a", "kubefleet-clusterresourceplacement", "kubefleet addon name") + f.StringArrayVarP(&dc.flagVariables, "variable", "", []string{}, "pass template variables (e.g. --variable CRP_NAME=demo-crp --variable PLACEMENT_TYPE=PickAll)") + + dc.templateWriter = &writers.LocalFSWriter{} + + return cmd +} + +func (dc *distributeCmd) run() error { + flagVariablesMap = flagVariablesToMap(dc.flagVariables) + + if dryRun { + distributeDryRunRecorder = dryrunpkg.NewDryRunRecorder() + dc.templateVariableRecorder = distributeDryRunRecorder + dc.templateWriter = distributeDryRunRecorder + } + + updatedDest, err := cmdhelpers.GetAddonDestPath(dc.dest) + if err != nil { + log.Errorf("error getting addon destination path: %s", err.Error()) + return err + } + + // Default to kubefleet-clusterresourceplacement addon, but allow other kubefleet addons + templateName := "kubefleet-clusterresourceplacement" + if dc.addon != "" { + templateName = dc.addon + } + + // Validate that the addon is a kubefleet addon + if templateName != "kubefleet-clusterresourceplacement" { + return fmt.Errorf("distribute command only supports kubefleet addons, got: %s", templateName) + } + + addonTemplate, err := handlers.GetTemplate(templateName, "", updatedDest, dc.templateWriter) + if err != nil { + log.Errorf("error getting kubefleet addon template: %s", err.Error()) + return err + } + if addonTemplate == nil { + return errors.New("DraftConfig is nil") + } + + addonTemplate.Config.VariableMapToDraftConfig(flagVariablesMap) + + err = cmdhelpers.PromptAddonValues(dc.dest, addonTemplate.Config) + if err != nil { + return err + } + + if dryRun { + for _, variable := range addonTemplate.Config.Variables { + dc.templateVariableRecorder.Record(variable.Name, variable.Value) + } + } + + err = addonTemplate.Generate() + if err != nil { + log.Errorf("error generating kubefleet addon template: %s", err.Error()) + return err + } + + if dryRun { + dryRunText, err := json.MarshalIndent(distributeDryRunRecorder.DryRunInfo, "", TWO_SPACES) + if err != nil { + return err + } + fmt.Println(string(dryRunText)) + if dryRunFile != "" { + log.Printf("writing dry run info to file %s", dryRunFile) + err = os.WriteFile(dryRunFile, dryRunText, 0644) + if err != nil { + return err + } + } + } + return err +} + +func init() { + rootCmd.AddCommand(newDistributeCmd()) +} \ No newline at end of file diff --git a/cmd/update.go b/cmd/update.go index 1230ee55b..9f126301d 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -70,31 +70,37 @@ func (uc *updateCmd) run() error { return err } - ingressTemplate, err := handlers.GetTemplate("app-routing-ingress", "", updatedDest, uc.templateWriter) + // Use the specified addon template, default to app-routing-ingress for backward compatibility + templateName := "app-routing-ingress" + if uc.addon != "" { + templateName = uc.addon + } + + addonTemplate, err := handlers.GetTemplate(templateName, "", updatedDest, uc.templateWriter) if err != nil { - log.Errorf("error getting ingress template: %s", err.Error()) + log.Errorf("error getting addon template: %s", err.Error()) return err } - if ingressTemplate == nil { + if addonTemplate == nil { return errors.New("DraftConfig is nil") } - ingressTemplate.Config.VariableMapToDraftConfig(flagVariablesMap) + addonTemplate.Config.VariableMapToDraftConfig(flagVariablesMap) - err = cmdhelpers.PromptAddonValues(uc.dest, ingressTemplate.Config) + err = cmdhelpers.PromptAddonValues(uc.dest, addonTemplate.Config) if err != nil { return err } if dryRun { - for _, variable := range ingressTemplate.Config.Variables { + for _, variable := range addonTemplate.Config.Variables { uc.templateVariableRecorder.Record(variable.Name, variable.Value) } } - err = ingressTemplate.Generate() + err = addonTemplate.Generate() if err != nil { - log.Errorf("error generating ingress template: %s", err.Error()) + log.Errorf("error generating addon template: %s", err.Error()) return err } diff --git a/docs/kubefleet-clusterresourceplacement.md b/docs/kubefleet-clusterresourceplacement.md new file mode 100644 index 000000000..c0dd89507 --- /dev/null +++ b/docs/kubefleet-clusterresourceplacement.md @@ -0,0 +1,135 @@ +# KubeFleet ClusterResourcePlacement Support + +Draft now supports generating KubeFleet ClusterResourcePlacement manifests through the `kubefleet-clusterresourceplacement` addon template. + +## Prerequisites + +1. Have an existing Draft project with deployment files (run `draft create` first) +2. Have the Draft CLI installed and built + +## Usage + +The ClusterResourcePlacement addon supports both PickAll and PickFixed placement types as described in the [KubeFleet documentation](https://kubefleet.dev/docs/concepts/crp/). + +### PickAll Placement Type + +For distributing resources to all matching clusters: + +```bash +draft distribute \ + --variable CRP_NAME=demo-crp \ + --variable RESOURCE_SELECTOR_NAME=ns-demo \ + --variable PLACEMENT_TYPE=PickAll \ + --variable PARTOF=my-project +``` + +This generates: + +```yaml +apiVersion: placement.kubernetes-fleet.io/v1 +kind: ClusterResourcePlacement +metadata: + name: demo-crp + labels: + app.kubernetes.io/name: demo-crp + app.kubernetes.io/part-of: my-project + kubernetes.azure.com/generator: draft +spec: + resourceSelectors: + - group: "" + kind: Namespace + name: ns-demo + version: v1 + policy: + placementType: PickAll +``` + +### PickFixed Placement Type + +For distributing resources to specific clusters: + +```bash +draft distribute \ + --variable CRP_NAME=ns-demo-crp \ + --variable RESOURCE_SELECTOR_NAME=fmad-demo \ + --variable PLACEMENT_TYPE=PickFixed \ + --variable CLUSTER_NAMES=cluster-name-01,cluster-name-02 \ + --variable PARTOF=my-project +``` + +This generates: + +```yaml +apiVersion: placement.kubernetes-fleet.io/v1 +kind: ClusterResourcePlacement +metadata: + name: ns-demo-crp + labels: + app.kubernetes.io/name: ns-demo-crp + app.kubernetes.io/part-of: my-project + kubernetes.azure.com/generator: draft +spec: + resourceSelectors: + - group: "" + kind: Namespace + name: ns-demo + version: v1 + policy: + placementType: PickFixed + clusterNames: + - cluster-name-01 + - cluster-name-02 +``` + +#### Example with Three Clusters + +```bash +draft distribute \ + --variable CRP_NAME=multi-cluster-demo \ + --variable RESOURCE_SELECTOR_NAME=demo-namespace \ + --variable PLACEMENT_TYPE=PickFixed \ + --variable CLUSTER_NAMES=cluster-east,cluster-west,cluster-central \ + --variable PARTOF=my-project +``` + +This generates: + +```yaml +apiVersion: placement.kubernetes-fleet.io/v1 +kind: ClusterResourcePlacement +metadata: + name: multi-cluster-demo + labels: + app.kubernetes.io/name: multi-cluster-demo + app.kubernetes.io/part-of: my-project + kubernetes.azure.com/generator: draft +spec: + resourceSelectors: + - group: "" + kind: Namespace + name: demo-namespace + version: v1 + policy: + placementType: PickFixed + clusterNames: + - cluster-east + - cluster-west + - cluster-central +``` + +## Template Variables + +| Variable | Type | Description | Required | Default | +|----------|------|-------------|----------|---------| +| `CRP_NAME` | string | Name of the ClusterResourcePlacement | Yes | - | +| `RESOURCE_SELECTOR_NAME` | string | Name of the resource to select for placement | Yes | - | +| `PLACEMENT_TYPE` | string | Placement policy type (PickAll or PickFixed) | No | "PickAll" | +| `CLUSTER_NAMES` | string | Comma-separated list of cluster names (for PickFixed only) | No | "" | +| `PARTOF` | string | Label to identify which project the resource belongs to | Yes | - | +| `GENERATORLABEL` | string | Label to identify who generated the resource | No | "draft" | + +Draft will prompt you for the required values. + +## Output + +The generated ClusterResourcePlacement manifest will be created at `manifests/clusterresourceplacement.yaml` in your project directory. diff --git a/example/clusterresourceplacement.go b/example/clusterresourceplacement.go new file mode 100644 index 000000000..9d27f2dfe --- /dev/null +++ b/example/clusterresourceplacement.go @@ -0,0 +1,66 @@ +package example + +import ( + "fmt" + + "github.com/Azure/draft/pkg/handlers" + "github.com/Azure/draft/pkg/templatewriter/writers" +) + +// WriteClusterResourcePlacementFilesExample shows how to set up a fileWriter and generate a fileMap using WriteClusterResourcePlacementFiles for Kubefleet +func WriteClusterResourcePlacementFilesExample() error { + // Create a file map + fileMap := make(map[string][]byte) + + // Create a template writer that writes to the file map + w := writers.FileMapWriter{ + FileMap: fileMap, + } + + // Select the kubefleet addon template type + templateType := "kubefleet-clusterresourceplacement" + + // Create a map of inputs to the template (must correspond to the inputs in the template/addons/kubefleet/clusterresourceplacement/draft.yaml file) + templateVars := map[string]string{ + "CRP_NAME": "example-crp", + "RESOURCE_SELECTOR_NAME": "example-namespace", + "PLACEMENT_TYPE": "PickFixed", + "CLUSTER_NAMES": "cluster-01,cluster-02,cluster-03", + "PARTOF": "example-project", + "GENERATORLABEL": "draft", + } + + // Set the output path for the ClusterResourcePlacement files + outputPath := "./" + + // Get the kubefleet template + template, err := handlers.GetTemplate(templateType, "", outputPath, &w) + if err != nil { + return fmt.Errorf("failed to get template: %e", err) + } + if template == nil { + return fmt.Errorf("template is nil") + } + + // Set the variable values within the template + for k, v := range templateVars { + template.Config.SetVariable(k, v) + } + + // Generate the ClusterResourcePlacement files + err = template.Generate() + if err != nil { + return fmt.Errorf("failed to generate manifest: %e", err) + } + + // Read written files from the file map + fmt.Printf("Files written in WriteClusterResourcePlacementFilesExample:\n") + for filePath, fileContents := range fileMap { + if fileContents == nil { + return fmt.Errorf("file contents for %s is nil", filePath) + } + fmt.Printf(" %s\n", filePath) // Print the file path + } + + return nil +} \ No newline at end of file diff --git a/example/clusterresourceplacement_test.go b/example/clusterresourceplacement_test.go new file mode 100644 index 000000000..0a674980b --- /dev/null +++ b/example/clusterresourceplacement_test.go @@ -0,0 +1,13 @@ +package example + +import ( + "testing" +) + +func TestWriteClusterResourcePlacementFilesExample(t *testing.T) { + err := WriteClusterResourcePlacementFilesExample() + if err != nil { + t.Errorf("WriteClusterResourcePlacementFilesExample failed: %e", err) + t.Fail() + } +} \ No newline at end of file diff --git a/pkg/fixtures/manifests/clusterresourceplacement/pickall/clusterresourceplacement.yaml b/pkg/fixtures/manifests/clusterresourceplacement/pickall/clusterresourceplacement.yaml new file mode 100644 index 000000000..22cb59993 --- /dev/null +++ b/pkg/fixtures/manifests/clusterresourceplacement/pickall/clusterresourceplacement.yaml @@ -0,0 +1,16 @@ +apiVersion: placement.kubernetes-fleet.io/v1 +kind: ClusterResourcePlacement +metadata: + name: demo-crp + labels: + app.kubernetes.io/name: demo-crp + app.kubernetes.io/part-of: test-app-project + kubernetes.azure.com/generator: draft +spec: + resourceSelectors: + - group: "" + kind: Namespace + name: fmad-demo + version: v1 + policy: + placementType: PickAll \ No newline at end of file diff --git a/pkg/fixtures/manifests/clusterresourceplacement/pickfixed/clusterresourceplacement.yaml b/pkg/fixtures/manifests/clusterresourceplacement/pickfixed/clusterresourceplacement.yaml new file mode 100644 index 000000000..f863494c8 --- /dev/null +++ b/pkg/fixtures/manifests/clusterresourceplacement/pickfixed/clusterresourceplacement.yaml @@ -0,0 +1,19 @@ +apiVersion: placement.kubernetes-fleet.io/v1 +kind: ClusterResourcePlacement +metadata: + name: fmad-demo-crp + labels: + app.kubernetes.io/name: fmad-demo-crp + app.kubernetes.io/part-of: test-app-project + kubernetes.azure.com/generator: draft +spec: + resourceSelectors: + - group: "" + kind: Namespace + name: fmad-demo + version: v1 + policy: + placementType: PickFixed + clusterNames: + - cluster-name-01 + - cluster-name-02 \ No newline at end of file diff --git a/pkg/handlers/template.go b/pkg/handlers/template.go index cf0472d40..f413678d6 100644 --- a/pkg/handlers/template.go +++ b/pkg/handlers/template.go @@ -166,8 +166,21 @@ func writeTemplate(draftTemplate *Template, inputFile string) error { return err } + // Define custom template functions + funcMap := tmpl.FuncMap{ + "split": func(delimiter, s string) []string { + if s == "" { + return []string{} + } + return strings.Split(s, delimiter) + }, + "trim": func(s string) string { + return strings.TrimSpace(s) + }, + } + // Parse the template file, missingkey=error ensures an error will be returned if any variable is missing during template execution. - tmpl, err := tmpl.New("template").Option("missingkey=error").Parse(string(file)) + tmpl, err := tmpl.New("template").Funcs(funcMap).Option("missingkey=error").Parse(string(file)) if err != nil { return err } diff --git a/pkg/handlers/templatetests/manifests_clusterresourceplacement_test.go b/pkg/handlers/templatetests/manifests_clusterresourceplacement_test.go new file mode 100644 index 000000000..410a098ac --- /dev/null +++ b/pkg/handlers/templatetests/manifests_clusterresourceplacement_test.go @@ -0,0 +1,45 @@ +package templatetests + +import ( + "testing" + + "github.com/Azure/draft/pkg/templatewriter/writers" +) + +func TestManifestsClusterResourcePlacementTemplates(t *testing.T) { + tests := []TestInput{ + { + Name: "valid clusterresourceplacement manifest with PickAll", + TemplateName: "kubefleet-clusterresourceplacement", + FixturesBaseDir: "../../fixtures/manifests/clusterresourceplacement/pickall", + Version: "0.0.1", + Dest: ".", + TemplateWriter: &writers.FileMapWriter{}, + VarMap: map[string]string{ + "CRP_NAME": "demo-crp", + "RESOURCE_SELECTOR_NAME": "fmad-demo", + "PLACEMENT_TYPE": "PickAll", + "PARTOF": "test-app-project", + }, + }, + { + Name: "valid clusterresourceplacement manifest with PickFixed", + TemplateName: "kubefleet-clusterresourceplacement", + FixturesBaseDir: "../../fixtures/manifests/clusterresourceplacement/pickfixed", + Version: "0.0.1", + Dest: ".", + TemplateWriter: &writers.FileMapWriter{}, + VarMap: map[string]string{ + "CRP_NAME": "fmad-demo-crp", + "RESOURCE_SELECTOR_NAME": "fmad-demo", + "PLACEMENT_TYPE": "PickFixed", + "CLUSTER_NAMES": "cluster-name-01,cluster-name-02", + "PARTOF": "test-app-project", + }, + }, + } + + for _, test := range tests { + RunTemplateTest(t, test) + } +} \ No newline at end of file diff --git a/pkg/prompts/prompts.go b/pkg/prompts/prompts.go index c889b770a..91740dd9d 100644 --- a/pkg/prompts/prompts.go +++ b/pkg/prompts/prompts.go @@ -43,6 +43,15 @@ func RunPromptsFromConfigWithSkipsIO(draftConfig *config.DraftConfig, Stdin io.R continue } + isVarActive, err := draftConfig.CheckActiveWhenConstraint(variable) + if err != nil { + return fmt.Errorf("unable to check ActiveWhen constraint: %w", err) + } + + if !isVarActive { + continue + } + if variable.Default.IsPromptDisabled { log.Debugf("Skipping prompt for %s as it has IsPromptDisabled=true", variable.Name) noPromptDefaultValue := GetVariableDefaultValue(draftConfig, variable) @@ -54,15 +63,6 @@ func RunPromptsFromConfigWithSkipsIO(draftConfig *config.DraftConfig, Stdin io.R continue } - isVarActive, err := draftConfig.CheckActiveWhenConstraint(variable) - if err != nil { - return fmt.Errorf("unable to check ActiveWhen constraint: %w", err) - } - - if !isVarActive { - continue - } - log.Debugf("constructing prompt for: %s", variable.Name) if variable.Type == "bool" { input, err := RunBoolPrompt(variable, Stdin, Stdout) diff --git a/template/addons/kubefleet/clusterresourceplacement/clusterresourceplacement.yaml b/template/addons/kubefleet/clusterresourceplacement/clusterresourceplacement.yaml new file mode 100644 index 000000000..790eb60ac --- /dev/null +++ b/template/addons/kubefleet/clusterresourceplacement/clusterresourceplacement.yaml @@ -0,0 +1,18 @@ +apiVersion: placement.kubernetes-fleet.io/v1 +kind: ClusterResourcePlacement +metadata: + name: {{ .Config.GetVariableValue "CRP_NAME" }} + labels: + app.kubernetes.io/name: {{ .Config.GetVariableValue "CRP_NAME" }} + app.kubernetes.io/part-of: {{ .Config.GetVariableValue "PARTOF" }} + kubernetes.azure.com/generator: {{ .Config.GetVariableValue "GENERATORLABEL" }} +spec: + resourceSelectors: + - group: "" + kind: Namespace + name: {{ .Config.GetVariableValue "RESOURCE_SELECTOR_NAME" }} + version: v1 + policy: + placementType: {{ .Config.GetVariableValue "PLACEMENT_TYPE" }}{{- if eq (.Config.GetVariableValue "PLACEMENT_TYPE") "PickFixed" }}{{- $clusterNames := .Config.GetVariableValue "CLUSTER_NAMES" }}{{- if ne $clusterNames "" }} + clusterNames:{{- range (split "," $clusterNames) }}{{- $cluster := . | trim }}{{- if ne $cluster "" }} + - {{ $cluster }}{{- end }}{{- end }}{{- end }}{{- end }} \ No newline at end of file diff --git a/template/addons/kubefleet/clusterresourceplacement/draft.yaml b/template/addons/kubefleet/clusterresourceplacement/draft.yaml new file mode 100644 index 000000000..7206a63ab --- /dev/null +++ b/template/addons/kubefleet/clusterresourceplacement/draft.yaml @@ -0,0 +1,50 @@ +templateName: "kubefleet-clusterresourceplacement" +description: "This template is used to create a Kubefleet ClusterResourcePlacement for managing resource placement across clusters" +versions: ["0.0.1"] +defaultVersion: "0.0.1" +type: "manifest" +variables: + - name: "CRP_NAME" + type: "string" + kind: "kubernetesResourceName" + description: "the name of the ClusterResourcePlacement" + versions: ">=0.0.1" + - name: "RESOURCE_SELECTOR_NAME" + type: "string" + kind: "kubernetesResourceName" + description: "the name of the resource to select for placement" + versions: ">=0.0.1" + - name: "PLACEMENT_TYPE" + type: "string" + kind: "label" + description: "the placement type for the policy (PickAll or PickFixed)" + versions: ">=0.0.1" + default: + value: "PickAll" + allowedValues: + - "PickAll" + - "PickFixed" + - name: "CLUSTER_NAMES" + type: "string" + kind: "label" + description: "comma-separated list of cluster names for PickFixed placement type (optional)" + versions: ">=0.0.1" + default: + disablePrompt: true + value: "" + activeWhen: + - variableName: "PLACEMENT_TYPE" + value: "PickFixed" + condition: "equals" + - name: "PARTOF" + type: "string" + kind: "label" + description: "the label to identify which project the resource belong to" + versions: ">=0.0.1" + - name: "GENERATORLABEL" + type: "string" + kind: "label" + description: "the label to identify who generated the resource" + versions: ">=0.0.1" + default: + value: "draft" \ No newline at end of file