diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c036d21b..900813dd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [master](https://github.com/arangodb/kube-arangodb/tree/master) (N/A) - (Bugfix) (Platform) Increase memory limit for Inventory - (Feature) (LM) Inventory Generator +- (Feature) (License) Activation CLI ## [1.3.1](https://github.com/arangodb/kube-arangodb/tree/1.3.1) (2025-10-07) - (Documentation) Add ArangoPlatformStorage Docs & Examples diff --git a/docs/cli/arangodb_operator_platform.md b/docs/cli/arangodb_operator_platform.md index 2ec8432a3..9b827fc76 100644 --- a/docs/cli/arangodb_operator_platform.md +++ b/docs/cli/arangodb_operator_platform.md @@ -14,7 +14,7 @@ Usage: Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command - license License Package related operations + license License related Operations package Release Package related operations Flags: @@ -41,6 +41,7 @@ Available Commands: import Imports the package from the ZIP format install Installs the specified setup of the platform merge Merges definitions into single file + registry Points all images to the new registry Flags: -h, --help help for package @@ -96,13 +97,16 @@ Global Flags: [START_INJECT]: # (arangodb_operator_platform_license_cmd) ``` -License Package related operations +License related Operations Usage: arangodb_operator_platform license [command] Available Commands: + activate Activates the License on ArangoDB Endpoint + generate Generate the License inventory Inventory Generator + secret Creates Platform Secret with Registry credentials Flags: -h, --help help for license @@ -138,3 +142,79 @@ Global Flags: -n, --namespace string Kubernetes Namespace (default "default") ``` [END_INJECT]: # (arangodb_operator_platform_license_inventory_cmd) + +# ArangoDB Operator Platform License Activate Command + +[START_INJECT]: # (arangodb_operator_platform_license_activate_cmd) +``` +Activates the License on ArangoDB Endpoint + +Usage: + arangodb_operator_platform license activate [flags] + +Flags: + --arango.authentication string Arango Endpoint Auth Method. One of: Disabled, Basic, Token (default "Disabled") + --arango.basic.password string Arango Password for Basic Authentication + --arango.basic.username string Arango Username for Basic Authentication + --arango.endpoint strings Arango Endpoint + --arango.insecure Arango Endpoint Insecure + --arango.token string Arango JWT Token for Authentication + -h, --help help for activate + --license.client.id string LicenseManager Client ID + --license.client.secret string LicenseManager Client Secret + --license.client.stage strings LicenseManager Stages (default [prd]) + --license.endpoint string LicenseManager Endpoint (default "license.arango.ai") + --license.interval duration Interval of the license synchronization + +Global Flags: + --kubeconfig string Kubernetes Config File + -n, --namespace string Kubernetes Namespace (default "default") +``` +[END_INJECT]: # (arangodb_operator_platform_license_activate_cmd) + +# ArangoDB Operator Platform License Generate Command + +[START_INJECT]: # (arangodb_operator_platform_license_generate_cmd) +``` +Generate the License + +Usage: + arangodb_operator_platform license generate [flags] + +Flags: + --deployment.id string Deployment ID + -h, --help help for generate + --inventory string Path to the Inventory File + --license.client.id string LicenseManager Client ID + --license.client.secret string LicenseManager Client Secret + --license.client.stage strings LicenseManager Stages (default [prd]) + --license.endpoint string LicenseManager Endpoint (default "license.arango.ai") + +Global Flags: + --kubeconfig string Kubernetes Config File + -n, --namespace string Kubernetes Namespace (default "default") +``` +[END_INJECT]: # (arangodb_operator_platform_license_generate_cmd) + +# ArangoDB Operator Platform License Secret Command + +[START_INJECT]: # (arangodb_operator_platform_license_secret_cmd) +``` +Creates Platform Secret with Registry credentials + +Usage: + arangodb_operator_platform license secret [flags] + +Flags: + -h, --help help for secret + --license.client.id string LicenseManager Client ID + --license.client.secret string LicenseManager Client Secret + --license.client.stage strings LicenseManager Stages (default [prd]) + --license.endpoint string LicenseManager Endpoint (default "license.arango.ai") + --secret string Kubernetes Secret Name + +Global Flags: + --kubeconfig string Kubernetes Config File + -n, --namespace string Kubernetes Namespace (default "default") +``` +[END_INJECT]: # (arangodb_operator_platform_license_secret_cmd) diff --git a/internal/docs_test.go b/internal/docs_test.go index e9340f82f..099233844 100644 --- a/internal/docs_test.go +++ b/internal/docs_test.go @@ -78,8 +78,8 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by els += 1 - write(t, out, "### %s\n\n", el.Path) - write(t, out, "Type: `%s` [\\[ref\\]](%s/%s#L%d)\n\n", el.Type, repositoryPath, el.File, el.Line) + writef(t, out, "### %s\n\n", el.Path) + writef(t, out, "Type: `%s` [\\[ref\\]](%s/%s#L%d)\n\n", el.Type, repositoryPath, el.File, el.Line) if grade := el.Grade; grade != nil { switch grade.Grade { @@ -88,7 +88,7 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by write(t, out, "> ***DEPRECATED***\n") write(t, out, "> \n") for _, line := range grade.Message { - write(t, out, "> **%s**\n", line) + writef(t, out, "> **%s**\n", line) } write(t, out, "\n") case DocDefinitionGradeAlpha: @@ -96,7 +96,7 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by write(t, out, "> ***ALPHA***\n") write(t, out, "> \n") for _, line := range grade.Message { - write(t, out, "> **%s**\n", line) + writef(t, out, "> **%s**\n", line) } write(t, out, "\n") case DocDefinitionGradeBeta: @@ -104,7 +104,7 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by write(t, out, "> ***BETA***\n") write(t, out, "> \n") for _, line := range grade.Message { - write(t, out, "> **%s**\n", line) + writef(t, out, "> **%s**\n", line) } write(t, out, "\n") } @@ -112,20 +112,20 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by if d := el.Important; d != nil { write(t, out, "> [!IMPORTANT]\n") - write(t, out, "> **%s**\n\n", *d) + writef(t, out, "> **%s**\n\n", *d) } if d := el.Required; d != nil { if *d == "" { write(t, out, "This field is **required**\n\n") } else { - write(t, out, "This field is **required**: %s\n\n", *d) + writef(t, out, "This field is **required**: %s\n\n", *d) } } if len(el.Docs) > 0 { for _, doc := range el.Docs { - write(t, out, "%s\n", doc) + writef(t, out, "%s\n", doc) } write(t, out, "\n") } @@ -136,9 +136,9 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by for _, link := range el.Links { z := goStrings.Split(link, "|") if len(z) == 1 { - write(t, out, "* [Documentation](%s)\n", z[0]) + writef(t, out, "* [Documentation](%s)\n", z[0]) } else if len(z) == 2 { - write(t, out, "* [%s](%s)\n", z[0], z[1]) + writef(t, out, "* [%s](%s)\n", z[0], z[1]) } else { require.Fail(t, "Invalid link format") } @@ -151,7 +151,7 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by write(t, out, "Example:\n") write(t, out, "```yaml\n") for _, example := range el.Example { - write(t, out, "%s\n", example) + writef(t, out, "%s\n", example) } write(t, out, "```\n\n") } @@ -167,9 +167,9 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by } if len(z) == 1 { - write(t, out, "* %s\n", snip) + writef(t, out, "* %s\n", snip) } else if len(z) == 2 { - write(t, out, "* %s - %s\n", snip, z[1]) + writef(t, out, "* %s - %s\n", snip, z[1]) } else { require.Fail(t, "Invalid enum format") } @@ -177,7 +177,7 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by write(t, out, "\n") } else { if d := el.Default; d != nil { - write(t, out, "Default Value: `%s`\n\n", *d) + writef(t, out, "Default Value: `%s`\n\n", *d) } } @@ -185,7 +185,7 @@ func (d DocDefinitions) RenderMarkdown(t *testing.T, repositoryPath string) []by if *d == "" { write(t, out, "This field is **immutable**\n\n") } else { - write(t, out, "This field is **immutable**: %s\n\n", *d) + writef(t, out, "This field is **immutable**: %s\n\n", *d) } } } @@ -545,10 +545,10 @@ func generateDocs(t *testing.T, objects map[string]map[string]interface{}, field "title": objName, "parent": apiIndexPageTitle, }) - write(t, out, "# API Reference for %s\n\n", objName) + writef(t, out, "# API Reference for %s\n\n", objName) util.IterateSorted(renderSections, func(name string, section []byte) { - write(t, out, "## %s\n\n", util.BoolSwitch(name == "", "Object", name)) + writef(t, out, "## %s\n\n", util.BoolSwitch(name == "", "Object", name)) _, err = out.Write(section) require.NoError(t, err) @@ -558,7 +558,12 @@ func generateDocs(t *testing.T, objects map[string]map[string]interface{}, field return outPaths } -func write(t *testing.T, out io.Writer, format string, args ...interface{}) { +func write(t *testing.T, out io.Writer, format string) { + _, err := out.Write([]byte(format)) + require.NoError(t, err) +} + +func writef(t *testing.T, out io.Writer, format string, args ...interface{}) { _, err := out.Write([]byte(fmt.Sprintf(format, args...))) require.NoError(t, err) } diff --git a/internal/readme_cli.go b/internal/readme_cli.go index 4bbb17eab..85889018f 100644 --- a/internal/readme_cli.go +++ b/internal/readme_cli.go @@ -121,6 +121,30 @@ func GenerateCLIArangoDBOperatorPlatformReadme(root string) error { readmeSections["arangodb_operator_platform_cmd"] = section } + if section, err := GenerateHelpQuoted(cmd, "license"); err != nil { + return err + } else { + readmeSections["arangodb_operator_platform_license_cmd"] = section + } + + if section, err := GenerateHelpQuoted(cmd, "license", "activate"); err != nil { + return err + } else { + readmeSections["arangodb_operator_platform_license_activate_cmd"] = section + } + + if section, err := GenerateHelpQuoted(cmd, "license", "generate"); err != nil { + return err + } else { + readmeSections["arangodb_operator_platform_license_generate_cmd"] = section + } + + if section, err := GenerateHelpQuoted(cmd, "license", "secret"); err != nil { + return err + } else { + readmeSections["arangodb_operator_platform_license_secret_cmd"] = section + } + if section, err := GenerateHelpQuoted(cmd, "package"); err != nil { return err } else { diff --git a/pkg/license/manager/client.go b/pkg/license/manager/client.go new file mode 100644 index 000000000..9f3dbafe7 --- /dev/null +++ b/pkg/license/manager/client.go @@ -0,0 +1,100 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package manager + +import ( + "context" + "fmt" + goHttp "net/http" + "time" + + "github.com/arangodb/go-driver" + "github.com/arangodb/go-driver/http" + + "github.com/arangodb/kube-arangodb/pkg/platform/inventory" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/arangod" + ugrpc "github.com/arangodb/kube-arangodb/pkg/util/grpc" + operatorHTTP "github.com/arangodb/kube-arangodb/pkg/util/http" +) + +func NewClient(endpoint, id, key string, mods ...util.Mod[goHttp.Transport]) (Client, error) { + transport := operatorHTTP.Transport(mods...) + + stageEndpoint := fmt.Sprintf("https://%s", endpoint) + + connConfig := http.ConnectionConfig{ + Transport: transport, + DontFollowRedirect: true, + Endpoints: []string{stageEndpoint}, + } + + conn, err := http.NewConnection(connConfig) + if err != nil { + return nil, err + } + + conn, err = conn.SetAuthentication(driver.BasicAuthentication(id, key)) + if err != nil { + return nil, err + } + + return NewClientFromConn(conn), nil +} + +func NewClientFromConn(conn driver.Connection) Client { + return client{ + conn: conn, + } +} + +type Client interface { + License(ctx context.Context, req LicenseRequest) (LicenseResponse, error) + + Registry(ctx context.Context) (RegistryResponse, error) +} + +type LicenseRequest struct { + DeploymentID *string `json:"deployment_id,omitempty"` + TTL *time.Duration `json:"ttl,omitempty"` + Inventory *ugrpc.Object[*inventory.Spec] `json:"inventory,omitempty"` +} + +type LicenseResponse struct { + ID string `json:"id"` + License string `json:"license"` +} + +type RegistryResponse struct { + Token string `json:"token"` +} + +type client struct { + conn driver.Connection +} + +func (c client) License(ctx context.Context, req LicenseRequest) (LicenseResponse, error) { + return arangod.PostRequest[LicenseRequest, LicenseResponse](ctx, c.conn, req, "_api", "v1", "license").AcceptCode(200).Response() +} + +func (c client) Registry(ctx context.Context) (RegistryResponse, error) { + return arangod.GetRequest[RegistryResponse](ctx, c.conn, "_api", "v1", "registry", "token").AcceptCode(200).Response() +} diff --git a/pkg/license/manager/registry.go b/pkg/license/manager/registry.go new file mode 100644 index 000000000..0c66317c2 --- /dev/null +++ b/pkg/license/manager/registry.go @@ -0,0 +1,63 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package manager + +import ( + "encoding/base64" + "fmt" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type Registry struct { + Auths map[string]RegistryAuth `json:"auths,omitempty"` +} + +type RegistryAuth struct { + Client string `json:"client"` + Auth string `json:"auth,omitempty"` +} + +func NewRegistryAuth(endpoint, username, password string, stages ...Stage) (*Registry, error) { + if len(stages) == 0 { + return nil, errors.Errorf("Enable Auth for at least one stage") + } + + var r Registry + + r.Auths = map[string]RegistryAuth{} + + ra := RegistryAuth{ + Client: username, + Auth: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))), + } + + for _, s := range stages { + domain, err := s.RegistryDomain(endpoint) + if err != nil { + return nil, err + } + + r.Auths[domain] = ra + } + + return &r, nil +} diff --git a/pkg/license/manager/stage.go b/pkg/license/manager/stage.go new file mode 100644 index 000000000..8774a73fd --- /dev/null +++ b/pkg/license/manager/stage.go @@ -0,0 +1,70 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package manager + +import ( + "fmt" + goStrings "strings" + + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type Stage int + +func ParseStages(s ...string) []Stage { + return util.FormatList(s, func(a string) Stage { + return ParseStage(a) + }) +} + +func ParseStage(s string) Stage { + switch goStrings.ToLower(s) { + case "dev": + return StageDev + case "qa": + return StageQA + case "prd": + return StagePrd + default: + return StageUnknown + } +} + +const ( + StageUnknown Stage = iota + StageDev + StageQA + StagePrd +) + +func (s Stage) RegistryDomain(domain string) (string, error) { + switch s { + case StageDev: + return fmt.Sprintf("dev.registry.%s", domain), nil + case StageQA: + return fmt.Sprintf("qa.registry.%s", domain), nil + case StagePrd: + return fmt.Sprintf("registry.%s", domain), nil + } + + return "", errors.Errorf("invalid stage") +} diff --git a/pkg/platform/flags.go b/pkg/platform/flags.go index 97e61af0f..2ca528436 100644 --- a/pkg/platform/flags.go +++ b/pkg/platform/flags.go @@ -22,6 +22,7 @@ package platform import ( "os" + "time" sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" "github.com/arangodb/kube-arangodb/pkg/util" @@ -45,6 +46,24 @@ var ( Persistent: true, } + flagSecret = cli.Flag[string]{ + Name: "secret", + Description: "Kubernetes Secret Name", + Default: "", + Check: func(in string) error { + if in == "" { + return nil + } + + if err := sharedApi.IsValidName(in); err != nil { + return errors.Errorf("Invalid secret name: %s", err.Error()) + } + + return nil + }, + Persistent: true, + } + flagPlatformName = cli.Flag[string]{ Name: "platform.name", Description: "Kubernetes Platform Name (name of the ArangoDeployment)", @@ -59,6 +78,33 @@ var ( }, } + flagInventory = cli.Flag[string]{ + Name: "inventory", + Description: "Path to the Inventory File", + Default: "", + Persistent: true, + Check: func(in string) error { + if in == "" { + return nil + } + _, err := os.Stat(in) + if err != nil { + return err + } + return nil + }, + } + + flagDeploymentID = cli.Flag[string]{ + Name: "deployment.id", + Description: "Deployment ID", + Default: "", + Persistent: false, + Check: func(in string) error { + return nil + }, + } + flagOutput = cli.Flag[string]{ Name: "output", Short: "o", @@ -100,6 +146,8 @@ var ( }, } + flagLicenseManager = cli.NewLicenseManager("license") + flagDeployment = cli.NewDeployment("arango") flagValues = cli.Flag[[]string]{ @@ -142,4 +190,17 @@ var ( Description: "List of boosted registries", Default: nil, } + + flagActivateInterval = cli.Flag[time.Duration]{ + Name: "license.interval", + Description: "Interval of the license synchronization", + Default: 0, + Persistent: false, + Check: func(in time.Duration) error { + if in < 0 { + return errors.New("License Generation Interval cannot be negative") + } + return nil + }, + } ) diff --git a/pkg/platform/license.go b/pkg/platform/license.go index 81fdd663d..6cac81698 100644 --- a/pkg/platform/license.go +++ b/pkg/platform/license.go @@ -21,16 +21,26 @@ package platform import ( + goHttp "net/http" + "reflect" + + "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/arangodb/go-driver" + + "github.com/arangodb/kube-arangodb/pkg/platform/inventory" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/arangod" "github.com/arangodb/kube-arangodb/pkg/util/cli" + "github.com/arangodb/kube-arangodb/pkg/util/globals" ) func license() (*cobra.Command, error) { var cmd cobra.Command cmd.Use = "license" - cmd.Short = "License Package related operations" + cmd.Short = "License related Operations" if err := cli.RegisterFlags(&cmd); err != nil { return nil, err @@ -38,9 +48,68 @@ func license() (*cobra.Command, error) { if err := withRegisterCommand(&cmd, licenseInventory, + licenseSecret, + licenseActivate, + licenseGenerate, ); err != nil { return nil, err } return &cmd, nil } + +func buildInventory(cmd *cobra.Command) (*inventory.Spec, error) { + logger.Info("Connecting to the server...") + + conn, err := flagDeployment.Connection(cmd) + if err != nil { + return nil, err + } + + resp, err := arangod.GetRequestWithTimeout[driver.VersionInfo](cmd.Context(), globals.GetGlobals().Timeouts().ArangoD().Get(), conn, "_api", "version"). + AcceptCode(goHttp.StatusOK). + Response() + if err != nil { + return nil, err + } + + logger.Info("Discovered Arango %s (%s)", resp.Version, resp.License) + + obj, err := inventory.FetchInventory(cmd.Context(), logger, 8, conn) + + if err != nil { + return nil, err + } + + obj = util.FilterList(obj, func(item *inventory.Item) bool { + return item != nil + }) + + did := util.FilterList(obj, util.MultiFilterList( + func(item *inventory.Item) bool { + return item.Type == "ARANGO_DEPLOYMENT" + }, + func(item *inventory.Item) bool { + v, ok := item.Dimensions["detail"] + return ok && v == "id" + }, + )) + + if len(did) != 1 { + return nil, errors.Errorf("Expected to find a single ARANGO_DEPLOYMENT ID") + } + + tz, err := did[0].GetValue().Type() + if err != nil { + return nil, err + } + + if tz != reflect.TypeFor[string]() { + return nil, errors.Errorf("Expected to find type for ARANGO_DEPLOYMENT ID") + } + + return &inventory.Spec{ + DeploymentId: did[0].GetValue().GetStr(), + Items: obj, + }, nil +} diff --git a/pkg/platform/license_activate.go b/pkg/platform/license_activate.go new file mode 100644 index 000000000..f608b4783 --- /dev/null +++ b/pkg/platform/license_activate.go @@ -0,0 +1,130 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package platform + +import ( + "time" + + "github.com/spf13/cobra" + + "github.com/arangodb/kube-arangodb/pkg/deployment/client" + "github.com/arangodb/kube-arangodb/pkg/license/manager" + "github.com/arangodb/kube-arangodb/pkg/logging" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/cli" + ugrpc "github.com/arangodb/kube-arangodb/pkg/util/grpc" +) + +func licenseActivate() (*cobra.Command, error) { + var cmd cobra.Command + + cmd.Use = "activate" + cmd.Short = "Activates the License on ArangoDB Endpoint" + + if err := cli.RegisterFlags(&cmd, flagLicenseManager, flagActivateInterval, flagDeployment); err != nil { + return nil, err + } + + cmd.RunE = getRunner().With(licenseActivateRun).Run + + return &cmd, nil +} + +func licenseActivateRun(cmd *cobra.Command, args []string) error { + mc, err := flagLicenseManager.Client(cmd) + if err != nil { + return err + } + + del, err := flagActivateInterval.Get(cmd) + if err != nil { + return err + } + + if del == 0 { + logger.Info("Activate Once") + + return licenseActivateExecute(cmd, logger, mc) + } + + intervalT := time.NewTicker(del) + defer intervalT.Stop() + + logger.Dur("interval", del).Info("Activate In interval") + + for { + if err := licenseActivateExecute(cmd, logger, mc); err != nil { + return err + } + + select { + case <-intervalT.C: + continue + case <-cmd.Context().Done(): + return nil + } + } +} + +func licenseActivateExecute(cmd *cobra.Command, logger logging.Logger, mc manager.Client) error { + conn, err := flagDeployment.Connection(cmd) + if err != nil { + return err + } + + c := client.NewClient(conn, logger) + + inv, err := buildInventory(cmd) + if err != nil { + return err + } + + l := logger.Str("DeploymentID", inv.DeploymentId) + + l.Info("Discovered DeploymentID") + + l.Info("Generating License") + + lic, err := mc.License(cmd.Context(), manager.LicenseRequest{ + DeploymentID: util.NewType(inv.DeploymentId), + Inventory: util.NewType(ugrpc.NewObject(inv)), + }) + if err != nil { + return err + } + + l = l.Str("LicenseID", lic.ID) + + l.Info("Activating license...") + + if err := c.SetLicense(cmd.Context(), lic.License, true); err != nil { + return err + } + + nlic, err := c.GetLicense(cmd.Context()) + if err != nil { + return err + } + + l.Str("hash", nlic.Hash).Info("Activated!") + + return nil +} diff --git a/pkg/platform/license_generate.go b/pkg/platform/license_generate.go new file mode 100644 index 000000000..660e20943 --- /dev/null +++ b/pkg/platform/license_generate.go @@ -0,0 +1,92 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package platform + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/arangodb/kube-arangodb/pkg/license/manager" + "github.com/arangodb/kube-arangodb/pkg/platform/inventory" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/cli" + ugrpc "github.com/arangodb/kube-arangodb/pkg/util/grpc" +) + +func licenseGenerate() (*cobra.Command, error) { + var cmd cobra.Command + + cmd.Use = "generate" + cmd.Short = "Generate the License" + + if err := cli.RegisterFlags(&cmd, flagLicenseManager, flagDeploymentID, flagInventory); err != nil { + return nil, err + } + + cmd.RunE = getRunner().With(licenseGenerateRun).Run + + return &cmd, nil +} + +func licenseGenerateRun(cmd *cobra.Command, args []string) error { + mc, err := flagLicenseManager.Client(cmd) + if err != nil { + return err + } + + did, err := flagDeploymentID.Get(cmd) + if err != nil { + return err + } + + var inv *inventory.Spec + + if invFile, err := flagInventory.Get(cmd); err != nil { + return err + } else if invFile != "" { + inv, err = ugrpc.UnmarshalFile[*inventory.Spec](invFile) + if err != nil { + return err + } + } + + l := logger.Str("ClusterID", did) + + l.Info("Generating License") + + lic, err := mc.License(cmd.Context(), manager.LicenseRequest{ + DeploymentID: util.NewType(did), + Inventory: util.NewType(ugrpc.NewObject(inv)), + }) + if err != nil { + return err + } + + l = l.Str("LicenseID", lic.ID) + + l.Info("License Generated and printed to STDERR") + + fmt.Fprint(os.Stderr, lic.License) + + return nil +} diff --git a/pkg/platform/license_inventory.go b/pkg/platform/license_inventory.go index 67df98a45..1ab78e859 100644 --- a/pkg/platform/license_inventory.go +++ b/pkg/platform/license_inventory.go @@ -21,22 +21,13 @@ package platform import ( - goHttp "net/http" "os" - "reflect" "github.com/spf13/cobra" - "github.com/arangodb/go-driver" - - "github.com/arangodb/kube-arangodb/pkg/platform/inventory" - "github.com/arangodb/kube-arangodb/pkg/util" - "github.com/arangodb/kube-arangodb/pkg/util/arangod" "github.com/arangodb/kube-arangodb/pkg/util/cli" "github.com/arangodb/kube-arangodb/pkg/util/errors" - "github.com/arangodb/kube-arangodb/pkg/util/globals" ugrpc "github.com/arangodb/kube-arangodb/pkg/util/grpc" - "github.com/arangodb/kube-arangodb/pkg/util/shutdown" ) func licenseInventory() (*cobra.Command, error) { @@ -59,57 +50,12 @@ func licenseInventoryRun(cmd *cobra.Command, args []string) error { return errors.Errorf("Invalid arguments") } - conn, err := flagDeployment.Connection(cmd) - if err != nil { - return err - } - - resp, err := arangod.GetRequestWithTimeout[driver.VersionInfo](cmd.Context(), globals.GetGlobals().Timeouts().ArangoD().Get(), conn, "_api", "version"). - AcceptCode(goHttp.StatusOK). - Response() - if err != nil { - return err - } - - logger.Info("Discovered Arango %s (%s)", resp.Version, resp.License) - - obj, err := inventory.FetchInventory(shutdown.Context(), logger, 8, conn) - + inv, err := buildInventory(cmd) if err != nil { return err } - obj = util.FilterList(obj, func(item *inventory.Item) bool { - return item != nil - }) - - did := util.FilterList(obj, util.MultiFilterList( - func(item *inventory.Item) bool { - return item.Type == "ARANGO_DEPLOYMENT" - }, - func(item *inventory.Item) bool { - v, ok := item.Dimensions["detail"] - return ok && v == "id" - }, - )) - - if len(did) != 1 { - return errors.Errorf("Expected to find a single ARANGO_DEPLOYMENT ID") - } - - tz, err := did[0].GetValue().Type() - if err != nil { - return err - } - - if tz != reflect.TypeFor[string]() { - return errors.Errorf("Expected to find type for ARANGO_DEPLOYMENT ID") - } - - d, err := ugrpc.Marshal(&inventory.Spec{ - DeploymentId: did[0].GetValue().GetStr(), - Items: obj, - }) + d, err := ugrpc.Marshal(inv) if err != nil { return err } diff --git a/pkg/platform/license_secret.go b/pkg/platform/license_secret.go new file mode 100644 index 000000000..75e8080f0 --- /dev/null +++ b/pkg/platform/license_secret.go @@ -0,0 +1,167 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package platform + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + "github.com/arangodb/kube-arangodb/pkg/license/manager" + "github.com/arangodb/kube-arangodb/pkg/util/cli" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/kerrors" +) + +func licenseSecret() (*cobra.Command, error) { + var cmd cobra.Command + + cmd.Use = "secret" + cmd.Short = "Creates Platform Secret with Registry credentials" + + if err := cli.RegisterFlags(&cmd, flagSecret, flagLicenseManager); err != nil { + return nil, err + } + + cmd.RunE = getRunner().With(licenseSecretRun).Run + + return &cmd, nil +} + +func licenseSecretRun(cmd *cobra.Command, args []string) error { + client, err := getKubernetesClient(cmd) + if err != nil { + return err + } + + name, err := flagSecret.Get(cmd) + if err != nil { + return err + } + + namespace, err := flagNamespace.Get(cmd) + if err != nil { + return err + } + + stages, err := flagLicenseManager.Stages(cmd) + if err != nil { + return err + } + + id, err := flagLicenseManager.ClientID(cmd) + if err != nil { + return err + } + + endpoint, err := flagLicenseManager.Endpoint(cmd) + if err != nil { + return err + } + + mc, err := flagLicenseManager.Client(cmd) + if err != nil { + return err + } + + secret, err := mc.Registry(cmd.Context()) + if err != nil { + return err + } + + logger.Info("Creating new Registry Token") + + r, err := manager.NewRegistryAuth(endpoint, id, secret.Token, manager.ParseStages(stages...)...) + if err != nil { + return err + } + + logger.Info("New Registry Token Created") + + data, err := json.Marshal(r) + if err != nil { + return err + } + + if name != "" { + sClient := client.Kubernetes().CoreV1().Secrets(namespace) + + l := logger.Str("namespace", namespace).Str("secret", name) + + if s, err := sClient.Get(cmd.Context(), name, meta.GetOptions{}); err != nil { + if !kerrors.IsNotFound(err) { + return err + } + + l.Info("Secret not found, creating") + + if _, err := sClient.Create(cmd.Context(), &core.Secret{ + ObjectMeta: meta.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: core.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": data, + }, + }, meta.CreateOptions{}); err != nil { + return err + } + + l.Info("Secret Created") + } else { + l.Info("Secret found, updating") + + s.Data = map[string][]byte{ + ".dockerconfigjson": data, + } + + if _, err := sClient.Update(cmd.Context(), s, meta.UpdateOptions{}); err != nil { + return err + } + + l.Info("Secret Updated") + } + } else { + resp, err := yaml.Marshal(&core.Secret{ + ObjectMeta: meta.ObjectMeta{ + Name: "name", + }, + Type: core.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": data, + }, + }) + if err != nil { + return err + } + + logger.Info("Create Secret Manually. Secret printed to STDERR") + + fmt.Fprint(os.Stderr, string(resp)) + } + + return nil +} diff --git a/pkg/platform/pack/export.go b/pkg/platform/pack/export.go index 8df3c4c82..131483f5d 100644 --- a/pkg/platform/pack/export.go +++ b/pkg/platform/pack/export.go @@ -168,7 +168,6 @@ func (r *exportPackageSet) exportPackage(name string, spec helm.PackageSpec) exe h.RunAsync(ctx, r.exportImage(v)) v.Registry = nil - v.Kind = "" chartProto.Images[k] = v } diff --git a/pkg/platform/pack/import.go b/pkg/platform/pack/import.go index 00395c1ef..a724990fb 100644 --- a/pkg/platform/pack/import.go +++ b/pkg/platform/pack/import.go @@ -103,10 +103,6 @@ func (i *importPackageSet) run(p Proto) executor.RunFunc { h.RunAsync(ctx, i.importManifest(src, v)) } - type valuesInterface struct { - Images ProtoImages `json:"images,omitempty"` - } - h.WaitForSubThreads(t) for k, v := range p.Charts { @@ -120,7 +116,7 @@ func (i *importPackageSet) run(p Proto) executor.RunFunc { pkgS.Chart = data pkgS.Version = v.Version - var versions valuesInterface + var versions ProtoValues versions.Images = map[string]ProtoImage{} diff --git a/pkg/platform/pack/proto.go b/pkg/platform/pack/proto.go index d9a9acb10..ba25f3a21 100644 --- a/pkg/platform/pack/proto.go +++ b/pkg/platform/pack/proto.go @@ -20,7 +20,11 @@ package pack -import "fmt" +import ( + "fmt" + + "github.com/arangodb/kube-arangodb/pkg/util" +) type Proto struct { Charts ProtoCharts `json:"charts,omitempty"` @@ -36,17 +40,21 @@ type ProtoChart struct { Images ProtoImages `json:"images,omitempty"` } +type ProtoValues struct { + Images ProtoImages `json:"images,omitempty"` +} + type ProtoImages map[string]ProtoImage type ProtoImage struct { Registry *string `json:"registry,omitempty"` - Image string `json:"image"` - Tag string `json:"tag"` - Kind string `json:"kind,omitempty"` + Image string `json:"image,omitempty"` + Tag string `json:"tag,omitempty"` + Kind *string `json:"kind,omitempty"` } func (p ProtoImage) IsTest() bool { - return p.Kind == "Test" + return util.OptionalType(p.Kind, "") == "Test" } func (p ProtoImage) GetShortImage() string { diff --git a/pkg/platform/pack/registry.go b/pkg/platform/pack/registry.go new file mode 100644 index 000000000..56c19c8ef --- /dev/null +++ b/pkg/platform/pack/registry.go @@ -0,0 +1,143 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package pack + +import ( + "context" + "sync" + + "github.com/arangodb/kube-arangodb/pkg/logging" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/executor" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/helm" +) + +func Registry(ctx context.Context, registry string, m helm.ChartManager, p helm.Package) (helm.Package, error) { + i := ®istryExport{ + registry: registry, + m: m, + } + + if err := executor.Run(ctx, logger, 8, i.run(p)); err != nil { + return helm.Package{}, err + } + + return i.p, nil +} + +type registryExport struct { + lock sync.Mutex + + registry string + + p helm.Package + + m helm.ChartManager +} + +func (i *registryExport) run(p helm.Package) executor.RunFunc { + return func(ctx context.Context, log logging.Logger, t executor.Thread, h executor.Handler) error { + for k, v := range p.Packages { + h.RunAsync(ctx, i.exportPackage(k, v)) + } + + return nil + } +} + +func (i *registryExport) exportPackage(name string, spec helm.PackageSpec) executor.RunFunc { + return func(ctx context.Context, log logging.Logger, t executor.Thread, h executor.Handler) error { + var pkgS helm.PackageSpec + + repo, ok := i.m.Get(name) + if !ok { + return errors.Errorf("Chart `%s` not found", name) + } + + ver, ok := repo.Get(spec.Version) + if !ok { + return errors.Errorf("Chart `%s=%s` not found", name, spec.Version) + } + + c, err := ver.Get(ctx) + if err != nil { + return err + } + + pkgS.Version = spec.Version + + chartData, err := c.Get() + if err != nil { + return err + } + + type valuesInterface struct { + Images ProtoImages `json:"images,omitempty"` + } + + protoImages, err := util.JSONRemarshal[map[string]any, valuesInterface](chartData.Chart().Values) + if err != nil { + return err + } + + var versions ProtoValues + + versions.Images = map[string]ProtoImage{} + + for k, v := range protoImages.Images { + if v.IsTest() { + logger.Str("image", v.GetImage()).Info("Skip Test Image") + continue + } + + versions.Images[k] = ProtoImage{ + Registry: util.NewType(i.registry), + } + } + + vData, err := helm.NewValues(versions) + if err != nil { + return err + } + + pkgS.Overrides = vData + + i.withPackage(func(in helm.Package) helm.Package { + if in.Packages == nil { + in.Packages = map[string]helm.PackageSpec{} + } + + in.Packages[name] = pkgS + + return in + }) + + return nil + } +} + +func (i *registryExport) withPackage(mod util.ModR[helm.Package]) { + i.lock.Lock() + defer i.lock.Unlock() + + i.p = mod(i.p) +} diff --git a/pkg/platform/package.go b/pkg/platform/package.go index 38f85aec7..228b8cced 100644 --- a/pkg/platform/package.go +++ b/pkg/platform/package.go @@ -42,6 +42,7 @@ func pkg() (*cobra.Command, error) { packageExport, packageImport, packageMerge, + packageRegistry, ); err != nil { return nil, err } diff --git a/pkg/platform/package_registry.go b/pkg/platform/package_registry.go new file mode 100644 index 000000000..1bb098779 --- /dev/null +++ b/pkg/platform/package_registry.go @@ -0,0 +1,83 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package platform + +import ( + "bytes" + "os" + + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + + "github.com/arangodb/kube-arangodb/pkg/platform/pack" + "github.com/arangodb/kube-arangodb/pkg/util/cli" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +func packageRegistry() (*cobra.Command, error) { + var cmd cobra.Command + + cmd.Use = "registry [flags] registry package output" + cmd.Short = "Points all images to the new registry" + + if err := cli.RegisterFlags(&cmd, flagPlatformEndpoint); err != nil { + return nil, err + } + + cmd.RunE = getRunner().With(packageRegistryRun).Run + + return &cmd, nil +} + +func packageRegistryRun(cmd *cobra.Command, args []string) error { + if len(args) != 3 { + return errors.Errorf("Invalid arguments") + } + + registry := args[0] + + pkg, err := getHelmPackages(args[1]) + if err != nil { + logger.Err(err).Error("Unable to read the file") + return err + } + + out := args[2] + + cm, err := getChartManager(cmd) + if err != nil { + return err + } + + p, err := pack.Registry(cmd.Context(), registry, cm, pkg) + if err != nil { + return err + } + + data, err := yaml.Marshal(p) + if err != nil { + return err + } + + data = bytes.Join([][]byte{[]byte("---\n\n"), data}, nil) + + return os.WriteFile(out, data, 0644) +} diff --git a/pkg/platform/runner.go b/pkg/platform/runner.go index 837162e83..ea38761ae 100644 --- a/pkg/platform/runner.go +++ b/pkg/platform/runner.go @@ -30,12 +30,15 @@ func getRunner() cli.Runner { logging.Runner, cli.ValidateFlags( flagNamespace, + flagSecret, flagPlatformName, flagPlatformEndpoint, flagOutput, flagUpgradeVersions, flagAll, flagValues, + flagDeployment, + flagLicenseManager, ), } } diff --git a/pkg/util/cli/flag.go b/pkg/util/cli/flag.go index a5179e82e..0b82561e3 100644 --- a/pkg/util/cli/flag.go +++ b/pkg/util/cli/flag.go @@ -22,6 +22,7 @@ package cli import ( "reflect" + "time" "github.com/spf13/cobra" @@ -101,24 +102,37 @@ func (f Flag[T]) Register(cmd *cobra.Command) error { flags = cmd.PersistentFlags() } - v := reflect.ValueOf(f.Default).Interface() - if s, ok := v.(string); ok { + v := reflect.TypeOf(f.Default) + + z := reflect.ValueOf(f.Default).Interface() + + if v == util.TypeOf[string]() { + v := z.(string) + if short := f.Short; short == "" { + flags.String(f.Name, v, f.Description) + } else { + flags.StringP(f.Name, short, v, f.Description) + } + } else if v == util.TypeOf[bool]() { + v := z.(bool) if short := f.Short; short == "" { - flags.String(f.Name, s, f.Description) + flags.Bool(f.Name, v, f.Description) } else { - flags.StringP(f.Name, short, s, f.Description) + flags.BoolP(f.Name, short, v, f.Description) } - } else if s, ok := v.(bool); ok { + } else if v == util.TypeOf[[]string]() { + v := z.([]string) if short := f.Short; short == "" { - flags.Bool(f.Name, s, f.Description) + flags.StringSlice(f.Name, v, f.Description) } else { - flags.BoolP(f.Name, short, s, f.Description) + flags.StringSliceP(f.Name, short, v, f.Description) } - } else if s, ok := v.([]string); ok { + } else if v == util.TypeOf[time.Duration]() { + v := z.(time.Duration) if short := f.Short; short == "" { - flags.StringSlice(f.Name, s, f.Description) + flags.Duration(f.Name, v, f.Description) } else { - flags.StringSliceP(f.Name, short, s, f.Description) + flags.DurationP(f.Name, short, v, f.Description) } } else { return errors.Errorf("Unsupported type for kind: %s", reflect.ValueOf(f.Default).Type().String()) @@ -140,8 +154,10 @@ func (f Flag[T]) Register(cmd *cobra.Command) error { } func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { - v := reflect.ValueOf(f.Default).Interface() - if _, ok := v.(string); ok { + + v := reflect.TypeOf(f.Default) + + if v == util.TypeOf[string]() { v, err := cmd.Flags().GetString(f.Name) if err != nil { return util.Default[T](), err @@ -153,7 +169,7 @@ func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { } return q, nil - } else if _, ok := v.([]string); ok { + } else if v == util.TypeOf[[]string]() { v, err := cmd.Flags().GetStringSlice(f.Name) if err != nil { return util.Default[T](), err @@ -165,7 +181,7 @@ func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { } return q, nil - } else if _, ok := v.(bool); ok { + } else if v == util.TypeOf[bool]() { v, err := cmd.Flags().GetBool(f.Name) if err != nil { return util.Default[T](), err @@ -176,6 +192,18 @@ func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { return util.Default[T](), errors.Errorf("Unable to parse type for kind: %s", reflect.ValueOf(f.Default).Type().String()) } + return q, nil + } else if v == util.TypeOf[time.Duration]() { + v, err := cmd.Flags().GetDuration(f.Name) + if err != nil { + return util.Default[T](), err + } + + q, ok := reflect.ValueOf(v).Interface().(T) + if !ok { + return util.Default[T](), errors.Errorf("Unable to parse type for kind: %s", reflect.ValueOf(f.Default).Type().String()) + } + return q, nil } else { return util.Default[T](), errors.Errorf("Unsupported type for kind: %s", reflect.ValueOf(f.Default).Type().String()) diff --git a/pkg/util/cli/lm.client.go b/pkg/util/cli/lm.client.go new file mode 100644 index 000000000..09fc845ea --- /dev/null +++ b/pkg/util/cli/lm.client.go @@ -0,0 +1,50 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package cli + +import "github.com/spf13/cobra" + +type licenseManagerClient struct { + clientID Flag[string] + stages Flag[[]string] + clientSecret Flag[string] +} + +func (l licenseManagerClient) GetName() string { + return "client" +} + +func (l licenseManagerClient) Register(cmd *cobra.Command) error { + return RegisterFlags( + cmd, + l.clientID, + l.clientSecret, + l.stages, + ) +} + +func (l licenseManagerClient) Validate(cmd *cobra.Command) error { + return ValidateFlags( + l.clientID, + l.clientSecret, + l.stages, + )(cmd, nil) +} diff --git a/pkg/util/cli/lm.go b/pkg/util/cli/lm.go new file mode 100644 index 000000000..614bdc0b9 --- /dev/null +++ b/pkg/util/cli/lm.go @@ -0,0 +1,164 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package cli + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/spf13/cobra" + + "github.com/arangodb/kube-arangodb/pkg/license/manager" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +func NewLicenseManager(prefix string) LicenseManager { + return licenseManager{ + endpoint: Flag[string]{ + Name: fmt.Sprintf("%s.endpoint", prefix), + Default: "license.arango.ai", + Description: "LicenseManager Endpoint", + Check: func(in string) error { + if len(in) == 0 { + return errors.Errorf("empty endpoint") + } + + return nil + }, + }, + + client: licenseManagerClient{ + clientID: Flag[string]{ + Name: fmt.Sprintf("%s.client.id", prefix), + Description: "LicenseManager Client ID", + Default: "", + Persistent: false, + Check: func(in string) error { + if in == "" { + return errors.New("Platform Client ID is required") + } + + return nil + }, + }, + + stages: Flag[[]string]{ + Name: fmt.Sprintf("%s.client.stage", prefix), + Description: "LicenseManager Stages", + Default: []string{"prd"}, + Persistent: false, + Check: func(in []string) error { + if len(in) == 0 { + return errors.New("At least one stage needs to be defined") + } + + return nil + }, + }, + + clientSecret: Flag[string]{ + Name: "license.client.secret", + Description: "LicenseManager Client Secret", + Default: "", + Persistent: false, + Check: func(in string) error { + if _, err := uuid.Parse(in); err != nil { + return err + } + + return nil + }, + }, + }, + } +} + +type LicenseManager interface { + FlagRegisterer + + Endpoint(cmd *cobra.Command) (string, error) + Stages(cmd *cobra.Command) ([]string, error) + + ClientID(cmd *cobra.Command) (string, error) + ClientSecret(cmd *cobra.Command) (string, error) + + Client(cmd *cobra.Command) (manager.Client, error) +} + +type licenseManager struct { + endpoint Flag[string] + + client licenseManagerClient +} + +func (l licenseManager) Endpoint(cmd *cobra.Command) (string, error) { + return l.endpoint.Get(cmd) +} + +func (l licenseManager) Stages(cmd *cobra.Command) ([]string, error) { + return l.client.stages.Get(cmd) +} + +func (l licenseManager) ClientID(cmd *cobra.Command) (string, error) { + return l.client.clientID.Get(cmd) +} + +func (l licenseManager) ClientSecret(cmd *cobra.Command) (string, error) { + return l.client.clientSecret.Get(cmd) +} + +func (l licenseManager) GetName() string { + return "lm" +} + +func (l licenseManager) Client(cmd *cobra.Command) (manager.Client, error) { + endpoint, err := l.endpoint.Get(cmd) + if err != nil { + return nil, err + } + + cid, err := l.client.clientID.Get(cmd) + if err != nil { + return nil, err + } + + cs, err := l.client.clientSecret.Get(cmd) + if err != nil { + return nil, err + } + + return manager.NewClient(endpoint, cid, cs) +} + +func (l licenseManager) Register(cmd *cobra.Command) error { + return RegisterFlags( + cmd, + l.endpoint, + l.client, + ) +} + +func (l licenseManager) Validate(cmd *cobra.Command) error { + return ValidateFlags( + l.endpoint, + l.client, + )(cmd, nil) +} diff --git a/pkg/util/grpc/marshal.go b/pkg/util/grpc/marshal.go index 0be0a59f1..384ed2ae2 100644 --- a/pkg/util/grpc/marshal.go +++ b/pkg/util/grpc/marshal.go @@ -21,6 +21,8 @@ package grpc import ( + "os" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "sigs.k8s.io/yaml" @@ -79,3 +81,12 @@ func Unmarshal[T proto.Message](data []byte, opts ...util.Mod[protojson.Unmarsha return v, nil } + +func UnmarshalFile[T proto.Message](path string, opts ...util.Mod[protojson.UnmarshalOptions]) (T, error) { + data, err := os.ReadFile(path) + if err != nil { + return util.Default[T](), err + } + + return Unmarshal[T](data, opts...) +} diff --git a/pkg/util/grpc/object.go b/pkg/util/grpc/object.go index 2831b1be9..d19449540 100644 --- a/pkg/util/grpc/object.go +++ b/pkg/util/grpc/object.go @@ -22,6 +22,10 @@ package grpc import "google.golang.org/protobuf/proto" +func NewObject[IN proto.Message](in IN) Object[IN] { + return Object[IN]{Object: in} +} + type Object[IN proto.Message] struct { Object IN }