From 1c7863701e6f9d1dc4ac20885f826ef022ba184c Mon Sep 17 00:00:00 2001 From: jamlo Date: Thu, 16 Jan 2025 13:47:33 -0500 Subject: [PATCH 1/3] Add Agent license inspect command Linear: https://linear.app/expanso/issue/ENG-498/license-path-configuration --- cmd/cli/agent/license/inspect.go | 103 +++++++++ cmd/cli/agent/license/root.go | 15 ++ cmd/cli/agent/root.go | 2 + pkg/config/types/generated_constants.go | 1 + pkg/config/types/generated_descriptions.go | 1 + pkg/config/types/orchestrator.go | 7 + pkg/publicapi/apimodels/agent.go | 6 + pkg/publicapi/client/v2/api_agent.go | 7 + pkg/publicapi/endpoint/agent/endpoint.go | 51 +++++ pkg/publicapi/endpoint/agent/endpoint_test.go | 215 ++++++++++++++++++ .../13_agent_license_inspect_suite_test.go | 98 ++++++++ ...icense_inspect_with_metadata_suite_test.go | 88 +++++++ ...ense_inspect_with_no_license_suite_test.go | 53 +++++ .../licenses/test-license-with-metadata.json | 3 + .../13_orchestrator_config_with_license.yaml | 12 + ...tor_config_with_license_with_metadata.yaml | 12 + ...5_orchestrator_config_with_no_license.yaml | 10 + 17 files changed, 684 insertions(+) create mode 100644 cmd/cli/agent/license/inspect.go create mode 100644 cmd/cli/agent/license/root.go create mode 100644 test_integration/13_agent_license_inspect_suite_test.go create mode 100644 test_integration/14_agent_license_inspect_with_metadata_suite_test.go create mode 100644 test_integration/15_agent_license_inspect_with_no_license_suite_test.go create mode 100644 test_integration/common_assets/licenses/test-license-with-metadata.json create mode 100644 test_integration/common_assets/nodes_configs/13_orchestrator_config_with_license.yaml create mode 100644 test_integration/common_assets/nodes_configs/14_orchestrator_config_with_license_with_metadata.yaml create mode 100644 test_integration/common_assets/nodes_configs/15_orchestrator_config_with_no_license.yaml diff --git a/cmd/cli/agent/license/inspect.go b/cmd/cli/agent/license/inspect.go new file mode 100644 index 0000000000..55d25d4fb6 --- /dev/null +++ b/cmd/cli/agent/license/inspect.go @@ -0,0 +1,103 @@ +package license + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/bacalhau-project/bacalhau/cmd/util" + "github.com/bacalhau-project/bacalhau/cmd/util/flags/cliflags" + "github.com/bacalhau-project/bacalhau/cmd/util/output" + "github.com/bacalhau-project/bacalhau/pkg/lib/collections" + "github.com/bacalhau-project/bacalhau/pkg/publicapi/client/v2" +) + +// AgentLicenseInspectOptions is a struct to support license command +type AgentLicenseInspectOptions struct { + OutputOpts output.NonTabularOutputOptions +} + +// NewAgentLicenseInspectOptions returns initialized Options +func NewAgentLicenseInspectOptions() *AgentLicenseInspectOptions { + return &AgentLicenseInspectOptions{ + OutputOpts: output.NonTabularOutputOptions{}, + } +} + +func NewAgentLicenseInspectCmd() *cobra.Command { + o := NewAgentLicenseInspectOptions() + licenseCmd := &cobra.Command{ + Use: "inspect", + Short: "Get the agent license information", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + cfg, err := util.SetupRepoConfig(cmd) + if err != nil { + return fmt.Errorf("failed to setup repo: %w", err) + } + api, err := util.GetAPIClientV2(cmd, cfg) + if err != nil { + return fmt.Errorf("failed to create api client: %w", err) + } + return o.runAgentLicense(cmd, api) + }, + } + licenseCmd.Flags().AddFlagSet(cliflags.OutputNonTabularFormatFlags(&o.OutputOpts)) + return licenseCmd +} + +// Run executes license command +func (o *AgentLicenseInspectOptions) runAgentLicense(cmd *cobra.Command, api client.API) error { + ctx := cmd.Context() + response, err := api.Agent().License(ctx) + if err != nil { + return fmt.Errorf("could not get agent license: %w", err) + } + + // For JSON/YAML output + if o.OutputOpts.Format == output.JSONFormat || o.OutputOpts.Format == output.YAMLFormat { + return output.OutputOneNonTabular(cmd, o.OutputOpts, response.LicenseClaims) + } + + // Create header data pairs for key-value output + headerData := []collections.Pair[string, any]{ + {Left: "Product", Right: response.Product}, + {Left: "License ID", Right: response.LicenseID}, + {Left: "Customer ID", Right: response.CustomerID}, + {Left: "Valid Until", Right: response.ExpiresAt.Format(time.DateOnly)}, + {Left: "Version", Right: response.LicenseVersion}, + } + + // Always show Capabilities + capabilitiesStr := "{}" + if len(response.Capabilities) > 0 { + var caps []string + for k, v := range response.Capabilities { + caps = append(caps, fmt.Sprintf("%s=%s", k, v)) + } + capabilitiesStr = strings.Join(caps, ", ") + } + headerData = append(headerData, collections.Pair[string, any]{ + Left: "Capabilities", + Right: capabilitiesStr, + }) + + // Always show Metadata + metadataStr := "{}" + if len(response.Metadata) > 0 { + var meta []string + for k, v := range response.Metadata { + meta = append(meta, fmt.Sprintf("%s=%s", k, v)) + } + metadataStr = strings.Join(meta, ", ") + } + headerData = append(headerData, collections.Pair[string, any]{ + Left: "Metadata", + Right: metadataStr, + }) + + output.KeyValue(cmd, headerData) + return nil +} diff --git a/cmd/cli/agent/license/root.go b/cmd/cli/agent/license/root.go new file mode 100644 index 0000000000..3166476dc2 --- /dev/null +++ b/cmd/cli/agent/license/root.go @@ -0,0 +1,15 @@ +package license + +import ( + "github.com/spf13/cobra" +) + +func NewAgentLicenseRootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "license", + Short: "Commands to interact with the orchestrator license", + } + + cmd.AddCommand(NewAgentLicenseInspectCmd()) + return cmd +} diff --git a/cmd/cli/agent/root.go b/cmd/cli/agent/root.go index 9e9c9d9660..dfaa8a1298 100644 --- a/cmd/cli/agent/root.go +++ b/cmd/cli/agent/root.go @@ -1,6 +1,7 @@ package agent import ( + "github.com/bacalhau-project/bacalhau/cmd/cli/agent/license" "github.com/spf13/cobra" "github.com/bacalhau-project/bacalhau/cmd/util/hook" @@ -17,5 +18,6 @@ func NewCmd() *cobra.Command { cmd.AddCommand(NewNodeCmd()) cmd.AddCommand(NewVersionCmd()) cmd.AddCommand(NewConfigCmd()) + cmd.AddCommand(license.NewAgentLicenseRootCmd()) return cmd } diff --git a/pkg/config/types/generated_constants.go b/pkg/config/types/generated_constants.go index e9c789f0cb..ac8a3d2eeb 100644 --- a/pkg/config/types/generated_constants.go +++ b/pkg/config/types/generated_constants.go @@ -86,6 +86,7 @@ const OrchestratorEnabledKey = "Orchestrator.Enabled" const OrchestratorEvaluationBrokerMaxRetryCountKey = "Orchestrator.EvaluationBroker.MaxRetryCount" const OrchestratorEvaluationBrokerVisibilityTimeoutKey = "Orchestrator.EvaluationBroker.VisibilityTimeout" const OrchestratorHostKey = "Orchestrator.Host" +const OrchestratorLicenseLocalPathKey = "Orchestrator.License.LocalPath" const OrchestratorNodeManagerDisconnectTimeoutKey = "Orchestrator.NodeManager.DisconnectTimeout" const OrchestratorNodeManagerManualApprovalKey = "Orchestrator.NodeManager.ManualApproval" const OrchestratorPortKey = "Orchestrator.Port" diff --git a/pkg/config/types/generated_descriptions.go b/pkg/config/types/generated_descriptions.go index 4b331d5607..e0de200613 100644 --- a/pkg/config/types/generated_descriptions.go +++ b/pkg/config/types/generated_descriptions.go @@ -88,6 +88,7 @@ var ConfigDescriptions = map[string]string{ OrchestratorEvaluationBrokerMaxRetryCountKey: "MaxRetryCount specifies the maximum number of times an evaluation can be retried before being marked as failed.", OrchestratorEvaluationBrokerVisibilityTimeoutKey: "VisibilityTimeout specifies how long an evaluation can be claimed before it's returned to the queue.", OrchestratorHostKey: "Host specifies the hostname or IP address on which the Orchestrator server listens for compute node connections.", + OrchestratorLicenseLocalPathKey: "LocalPath specifies the local license file path", OrchestratorNodeManagerDisconnectTimeoutKey: "DisconnectTimeout specifies how long to wait before considering a node disconnected.", OrchestratorNodeManagerManualApprovalKey: "ManualApproval, if true, requires manual approval for new compute nodes joining the cluster.", OrchestratorPortKey: "Host specifies the port number on which the Orchestrator server listens for compute node connections.", diff --git a/pkg/config/types/orchestrator.go b/pkg/config/types/orchestrator.go index e854c523a6..80c1d91736 100644 --- a/pkg/config/types/orchestrator.go +++ b/pkg/config/types/orchestrator.go @@ -19,6 +19,8 @@ type Orchestrator struct { EvaluationBroker EvaluationBroker `yaml:"EvaluationBroker,omitempty" json:"EvaluationBroker,omitempty"` // SupportReverseProxy configures the orchestrator node to run behind a reverse proxy SupportReverseProxy bool `yaml:"SupportReverseProxy,omitempty" json:"SupportReverseProxy,omitempty"` + // License specifies license configuration for orchestrator node + License License `yaml:"License,omitempty" json:"License,omitempty"` } type OrchestratorAuth struct { @@ -76,3 +78,8 @@ type EvaluationBroker struct { // MaxRetryCount specifies the maximum number of times an evaluation can be retried before being marked as failed. MaxRetryCount int `yaml:"MaxRetryCount,omitempty" json:"MaxRetryCount,omitempty"` } + +type License struct { + // LocalPath specifies the local license file path + LocalPath string `yaml:"LocalPath,omitempty" json:"LocalPath,omitempty"` +} diff --git a/pkg/publicapi/apimodels/agent.go b/pkg/publicapi/apimodels/agent.go index 0c3974e4c9..69a21a9d69 100644 --- a/pkg/publicapi/apimodels/agent.go +++ b/pkg/publicapi/apimodels/agent.go @@ -2,6 +2,7 @@ package apimodels import ( "github.com/bacalhau-project/bacalhau/pkg/config/types" + "github.com/bacalhau-project/bacalhau/pkg/lib/license" "github.com/bacalhau-project/bacalhau/pkg/models" ) @@ -38,3 +39,8 @@ type GetAgentConfigResponse struct { BaseGetResponse Config types.Bacalhau `json:"config"` } + +type GetAgentLicenseResponse struct { + BaseGetResponse + *license.LicenseClaims +} diff --git a/pkg/publicapi/client/v2/api_agent.go b/pkg/publicapi/client/v2/api_agent.go index 9f0abc1457..6e64110f70 100644 --- a/pkg/publicapi/client/v2/api_agent.go +++ b/pkg/publicapi/client/v2/api_agent.go @@ -36,3 +36,10 @@ func (c *Agent) Config(ctx context.Context) (*apimodels.GetAgentConfigResponse, err := c.client.Get(ctx, "/api/v1/agent/config", &apimodels.BaseGetRequest{}, &res) return &res, err } + +// License is used to get the agent (orchestrator) license info. +func (c *Agent) License(ctx context.Context) (*apimodels.GetAgentLicenseResponse, error) { + var res apimodels.GetAgentLicenseResponse + err := c.client.Get(ctx, "/api/v1/agent/license", &apimodels.BaseGetRequest{}, &res) + return &res, err +} diff --git a/pkg/publicapi/endpoint/agent/endpoint.go b/pkg/publicapi/endpoint/agent/endpoint.go index a570b128f3..5cbaa6be73 100644 --- a/pkg/publicapi/endpoint/agent/endpoint.go +++ b/pkg/publicapi/endpoint/agent/endpoint.go @@ -1,13 +1,16 @@ package agent import ( + "encoding/json" "fmt" "net/http" + "os" "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" "github.com/bacalhau-project/bacalhau/pkg/config/types" + "github.com/bacalhau-project/bacalhau/pkg/lib/license" "github.com/bacalhau-project/bacalhau/pkg/models" "github.com/bacalhau-project/bacalhau/pkg/publicapi/apimodels" "github.com/bacalhau-project/bacalhau/pkg/publicapi/middleware" @@ -44,6 +47,7 @@ func NewEndpoint(params EndpointParams) *Endpoint { g.GET("/node", e.node) g.GET("/debug", e.debug) g.GET("/config", e.config) + g.GET("/license", e.license) return e } @@ -138,3 +142,50 @@ func (e *Endpoint) config(c echo.Context) error { Config: cfg, }) } + +// license godoc +// +// @ID agent/license +// @Summary Returns the details of the current configured orchestrator license. +// @Tags Ops +// @Produce json +// @Success 200 {object} license.LicenseClaims +// @Failure 404 {object} string "Node license not configured" +// @Failure 500 {object} string +// @Router /api/v1/agent/license [get] +func (e *Endpoint) license(c echo.Context) error { + // Get license path from config + licensePath := e.bacalhauConfig.Orchestrator.License.LocalPath + if licensePath == "" { + return echo.NewHTTPError(http.StatusNotFound, "Node license not configured") + } + + // Read license file + licenseData, err := os.ReadFile(licensePath) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to read license file: %s", err)) + } + + // Parse license JSON + var licenseFile struct { + License string `json:"license"` + } + if err := json.Unmarshal(licenseData, &licenseFile); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to parse license file: %s", err)) + } + + // Create validator and validate license + validator, err := license.NewOfflineLicenseValidator() + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create license validator: %s", err)) + } + + claims, err := validator.ValidateToken(licenseFile.License) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to validate license: %s", err)) + } + + return c.JSON(http.StatusOK, apimodels.GetAgentLicenseResponse{ + LicenseClaims: claims, + }) +} diff --git a/pkg/publicapi/endpoint/agent/endpoint_test.go b/pkg/publicapi/endpoint/agent/endpoint_test.go index 937a718acf..1cabfec751 100644 --- a/pkg/publicapi/endpoint/agent/endpoint_test.go +++ b/pkg/publicapi/endpoint/agent/endpoint_test.go @@ -4,8 +4,11 @@ package agent import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" "github.com/labstack/echo/v4" @@ -16,6 +19,8 @@ import ( "github.com/bacalhau-project/bacalhau/pkg/publicapi/apimodels" ) +const validOfficialTestLicense = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6ImU2NmQxZjNhLWE4ZDgtNGQ1Ny04ZjE0LTAwNzIyODQ0YWZlMiIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6e30sImlhdCI6MTczNjg4MTYzOCwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODgxNjM4LCJqdGkiOiJlNjZkMWYzYS1hOGQ4LTRkNTctOGYxNC0wMDcyMjg0NGFmZTIifQ.U6qkWmki2wp3RbPdn8d0zzsy4FchZIyUDmJi2bJ4w4vhwJlJ0_F2_317v4iPzy9q69eJOKNaqj8P3xYaPbpiooFm15OdJ3ecbMy8bKvvWVj43stw6HNP_uoW-RlZnY2zTOQ9WhlOhjnUPPC-UXOcaMwxiLBwMo5n3Rs0W9uAQHGQIptGg0sKiZvIrMZZ3vww2PZ3wJDiDvznE2lPtI7jAbcFFKDlhY3UiXed2ihGTWvLW8Zwj4veCR4PAUoEDu-nfQDvlqNeAvABT-KrKY2M-d5T_WzK1WwXtHok9tG2OV5ybSZoxFDQW3iqiCg6TqMwCAa6C6MBXtLnv-NP1H9Ytg" + // TestEndpointConfigRedactFields asserts that auth tokens in the config are redacted. func TestEndpointConfigRedactFields(t *testing.T) { router := echo.New() @@ -50,3 +55,213 @@ func TestEndpointConfigRedactFields(t *testing.T) { assert.Equal(t, payload.Config.Orchestrator.Auth.Token, "") assert.Equal(t, payload.Config.Compute.Auth.Token, "") } + +// TestEndpointLicense tests the license endpoint +func TestEndpointLicense(t *testing.T) { + // Create a temporary license file + tmpDir := t.TempDir() + licensePath := filepath.Join(tmpDir, "license.json") + + // Create license file with proper JSON format + licenseContent := fmt.Sprintf(`{ + "license": %q + }`, validOfficialTestLicense) + + err := os.WriteFile(licensePath, []byte(licenseContent), 0644) + require.NoError(t, err) + + router := echo.New() + + NewEndpoint(EndpointParams{ + Router: router, + BacalhauConfig: types.Bacalhau{ + Orchestrator: types.Orchestrator{ + License: types.License{ + LocalPath: licensePath, + }, + }, + }, + }) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/agent/license", nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + require.Equal(t, http.StatusOK, rr.Code) + + var response apimodels.GetAgentLicenseResponse + err = json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + + // Verify some basic claims + assert.Equal(t, "Bacalhau", response.Product) + assert.Equal(t, "e66d1f3a-a8d8-4d57-8f14-00722844afe2", response.LicenseID) + assert.Equal(t, "standard", response.LicenseType) + assert.Equal(t, "test-customer-id-123", response.CustomerID) + assert.Equal(t, "v1", response.LicenseVersion) + assert.Equal(t, "1", response.Capabilities["max_nodes"]) + assert.Equal(t, "https://expanso.io/", response.Issuer) + assert.Equal(t, "test-customer-id-123", response.Subject) + assert.Equal(t, "e66d1f3a-a8d8-4d57-8f14-00722844afe2", response.ID) + assert.Equal(t, int64(2384881638), response.ExpiresAt.Unix()) +} + +// TestEndpointLicenseNoLicense tests the license endpoint when no license is configured +func TestEndpointLicenseNoLicense(t *testing.T) { + router := echo.New() + + NewEndpoint(EndpointParams{ + Router: router, + BacalhauConfig: types.Bacalhau{ + Orchestrator: types.Orchestrator{ + License: types.License{ + // No license path configured + }, + }, + }, + }) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/agent/license", nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + require.Equal(t, http.StatusNotFound, rr.Code) + + // Also verify the error message + var errResp map[string]interface{} + err := json.NewDecoder(rr.Body).Decode(&errResp) + require.NoError(t, err) + assert.Equal(t, "Node license not configured", errResp["message"]) +} + +// TestEndpointLicenseFileNotFound tests when the license file doesn't exist +func TestEndpointLicenseFileNotFound(t *testing.T) { + router := echo.New() + + NewEndpoint(EndpointParams{ + Router: router, + BacalhauConfig: types.Bacalhau{ + Orchestrator: types.Orchestrator{ + License: types.License{ + LocalPath: "/non/existent/path/license.json", + }, + }, + }, + }) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/agent/license", nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + require.Equal(t, http.StatusInternalServerError, rr.Code) + + var errResp map[string]interface{} + err := json.NewDecoder(rr.Body).Decode(&errResp) + require.NoError(t, err) + assert.Contains(t, errResp["message"].(string), "failed to read license file") +} + +// TestEndpointLicenseInvalidJSON tests when the license file contains invalid JSON +func TestEndpointLicenseInvalidJSON(t *testing.T) { + tmpDir := t.TempDir() + licensePath := filepath.Join(tmpDir, "license.json") + + // Write invalid JSON to the license file + err := os.WriteFile(licensePath, []byte("invalid json content"), 0644) + require.NoError(t, err) + + router := echo.New() + + NewEndpoint(EndpointParams{ + Router: router, + BacalhauConfig: types.Bacalhau{ + Orchestrator: types.Orchestrator{ + License: types.License{ + LocalPath: licensePath, + }, + }, + }, + }) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/agent/license", nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + require.Equal(t, http.StatusInternalServerError, rr.Code) + + var errResp map[string]interface{} + err = json.NewDecoder(rr.Body).Decode(&errResp) + require.NoError(t, err) + assert.Contains(t, errResp["message"].(string), "failed to parse license file") +} + +// TestEndpointLicenseInvalidLicenseFormat tests when the license file is valid JSON but missing the license field +func TestEndpointLicenseInvalidLicenseFormat(t *testing.T) { + tmpDir := t.TempDir() + licensePath := filepath.Join(tmpDir, "license.json") + + // Write JSON without the required "license" field + err := os.WriteFile(licensePath, []byte(`{"some_other_field": "value"}`), 0644) + require.NoError(t, err) + + router := echo.New() + + NewEndpoint(EndpointParams{ + Router: router, + BacalhauConfig: types.Bacalhau{ + Orchestrator: types.Orchestrator{ + License: types.License{ + LocalPath: licensePath, + }, + }, + }, + }) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/agent/license", nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + require.Equal(t, http.StatusInternalServerError, rr.Code) + + var errResp map[string]interface{} + err = json.NewDecoder(rr.Body).Decode(&errResp) + require.NoError(t, err) + assert.Contains(t, errResp["message"].(string), "failed to validate license") +} + +// TestEndpointLicenseInvalidToken tests when the license token is invalid +func TestEndpointLicenseInvalidToken(t *testing.T) { + tmpDir := t.TempDir() + licensePath := filepath.Join(tmpDir, "license.json") + + // Write JSON with an invalid JWT token + licenseContent := fmt.Sprintf(`{ + "license": "invalid.jwt.token" + }`) + err := os.WriteFile(licensePath, []byte(licenseContent), 0644) + require.NoError(t, err) + + router := echo.New() + + NewEndpoint(EndpointParams{ + Router: router, + BacalhauConfig: types.Bacalhau{ + Orchestrator: types.Orchestrator{ + License: types.License{ + LocalPath: licensePath, + }, + }, + }, + }) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/agent/license", nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + require.Equal(t, http.StatusInternalServerError, rr.Code) + + var errResp map[string]interface{} + err = json.NewDecoder(rr.Body).Decode(&errResp) + require.NoError(t, err) + assert.Contains(t, errResp["message"].(string), "failed to validate license: failed to parse license: token is malformed") +} diff --git a/test_integration/13_agent_license_inspect_suite_test.go b/test_integration/13_agent_license_inspect_suite_test.go new file mode 100644 index 0000000000..24de51ff46 --- /dev/null +++ b/test_integration/13_agent_license_inspect_suite_test.go @@ -0,0 +1,98 @@ +package test_integration + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" +) + +type AgentLicenseInspectSuite struct { + BaseDockerComposeTestSuite +} + +func NewAgentLicenseInspectSuite() *AgentLicenseInspectSuite { + s := &AgentLicenseInspectSuite{} + s.GlobalRunIdentifier = globalTestExecutionId + s.SuiteRunIdentifier = strings.ToLower(strings.Split(uuid.New().String(), "-")[0]) + return s +} + +func (s *AgentLicenseInspectSuite) SetupSuite() { + rawDockerComposeFilePath := "./common_assets/docker_compose_files/orchestrator-node-with-custom-start-command.yml" + s.Context, s.Cancel = context.WithCancel(context.Background()) + + orchestratorConfigFile := s.commonAssets("nodes_configs/13_orchestrator_config_with_license.yaml") + orchestratorStartCommand := fmt.Sprintf("bacalhau serve --config=%s", orchestratorConfigFile) + extraRenderingData := map[string]interface{}{ + "OrchestratorStartCommand": orchestratorStartCommand, + } + s.BaseDockerComposeTestSuite.SetupSuite(rawDockerComposeFilePath, extraRenderingData) +} + +func (s *AgentLicenseInspectSuite) TearDownSuite() { + s.T().Log("Tearing down [Test Suite] in AgentLicenseInspectSuite...") + s.BaseDockerComposeTestSuite.TearDownSuite() +} + +func (s *AgentLicenseInspectSuite) TestValidateRemoteLicense() { + agentLicenseInspectionOutput, err := s.executeCommandInDefaultJumpbox( + []string{ + "bacalhau", "agent", "license", "inspect", + }, + ) + s.Require().NoErrorf(err, "Error inspecting license: %q", err) + + expectedOutput := `Product = Bacalhau +License ID = e66d1f3a-a8d8-4d57-8f14-00722844afe2 +Customer ID = test-customer-id-123 +Valid Until = 2045-07-28 +Version = v1 +Capabilities = max_nodes=1 +Metadata = {}` + + s.Require().Contains(agentLicenseInspectionOutput, expectedOutput) +} + +func (s *AgentLicenseInspectSuite) TestValidateAgentLicenseJSONOutput() { + agentLicenseInspectionOutput, err := s.executeCommandInDefaultJumpbox( + []string{ + "bacalhau", + "agent", + "license", + "inspect", + "--output=json", + }, + ) + s.Require().NoErrorf(err, "Error inspecting license: %q", err) + + output, err := s.convertStringToDynamicJSON(agentLicenseInspectionOutput) + s.Require().NoError(err) + + productName, err := output.Query("$.product") + s.Require().NoError(err) + s.Require().Equal("Bacalhau", productName.String()) + + licenseID, err := output.Query("$.license_id") + s.Require().NoError(err) + s.Require().Equal("e66d1f3a-a8d8-4d57-8f14-00722844afe2", licenseID.String()) + + customerID, err := output.Query("$.customer_id") + s.Require().NoError(err) + s.Require().Equal("test-customer-id-123", customerID.String()) + + licenseVersion, err := output.Query("$.license_version") + s.Require().NoError(err) + s.Require().Equal("v1", licenseVersion.String()) + + capabilitiesMaxNodes, err := output.Query("$.capabilities.max_nodes") + s.Require().NoError(err) + s.Require().Equal("1", capabilitiesMaxNodes.String()) +} + +func TestAgentLicenseInspectSuite(t *testing.T) { + suite.Run(t, NewAgentLicenseInspectSuite()) +} diff --git a/test_integration/14_agent_license_inspect_with_metadata_suite_test.go b/test_integration/14_agent_license_inspect_with_metadata_suite_test.go new file mode 100644 index 0000000000..2fcb8f0ef5 --- /dev/null +++ b/test_integration/14_agent_license_inspect_with_metadata_suite_test.go @@ -0,0 +1,88 @@ +package test_integration + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" +) + +type AgentLicenseInspectWithMetadataSuite struct { + BaseDockerComposeTestSuite +} + +func NewAgentLicenseInspectWithMetadataSuite() *AgentLicenseInspectWithMetadataSuite { + s := &AgentLicenseInspectWithMetadataSuite{} + s.GlobalRunIdentifier = globalTestExecutionId + s.SuiteRunIdentifier = strings.ToLower(strings.Split(uuid.New().String(), "-")[0]) + return s +} + +func (s *AgentLicenseInspectWithMetadataSuite) SetupSuite() { + rawDockerComposeFilePath := "./common_assets/docker_compose_files/orchestrator-node-with-custom-start-command.yml" + s.Context, s.Cancel = context.WithCancel(context.Background()) + + orchestratorConfigFile := s.commonAssets("nodes_configs/14_orchestrator_config_with_license_with_metadata.yaml") + orchestratorStartCommand := fmt.Sprintf("bacalhau serve --config=%s", orchestratorConfigFile) + extraRenderingData := map[string]interface{}{ + "OrchestratorStartCommand": orchestratorStartCommand, + } + s.BaseDockerComposeTestSuite.SetupSuite(rawDockerComposeFilePath, extraRenderingData) +} + +func (s *AgentLicenseInspectWithMetadataSuite) TearDownSuite() { + s.T().Log("Tearing down [Test Suite] in AgentLicenseInspectWithMetadataSuite...") + s.BaseDockerComposeTestSuite.TearDownSuite() +} + +func (s *AgentLicenseInspectWithMetadataSuite) TestValidateRemoteLicenseWithMetadata() { + agentLicenseInspectionOutput, err := s.executeCommandInDefaultJumpbox( + []string{ + "bacalhau", "agent", "license", "inspect", + }, + ) + s.Require().NoErrorf(err, "Error inspecting license: %q", err) + + expectedOutput := `Product = Bacalhau +License ID = 2d58c7c9-ec29-45a5-a5cd-cb8f7fee6678 +Customer ID = test-customer-id-123 +Valid Until = 2045-07-28 +Version = v1 +Capabilities = max_nodes=1 +Metadata = someMetadata=valueOfSomeMetadata` + + s.Require().Contains(agentLicenseInspectionOutput, expectedOutput) +} + +func (s *AgentLicenseInspectWithMetadataSuite) TestValidateAgentLicenseYAMLOutput() { + agentLicenseInspectionOutput, err := s.executeCommandInDefaultJumpbox( + []string{ + "bacalhau", + "agent", + "license", + "inspect", + "--output=yaml", + }, + ) + s.Require().NoErrorf(err, "Error inspecting license: %q", err) + + s.Require().Contains(agentLicenseInspectionOutput, "someMetadata: valueOfSomeMetadata") + s.Require().Contains(agentLicenseInspectionOutput, "jti: 2d58c7c9-ec29-45a5-a5cd-cb8f7fee6678") + s.Require().Contains(agentLicenseInspectionOutput, "iss: https://expanso.io/") + s.Require().Contains(agentLicenseInspectionOutput, "iat: 1736889682") + s.Require().Contains(agentLicenseInspectionOutput, "exp: 2384889682") + s.Require().Contains(agentLicenseInspectionOutput, "customer_id: test-customer-id-123") + s.Require().Contains(agentLicenseInspectionOutput, "license_id: 2d58c7c9-ec29-45a5-a5cd-cb8f7fee6678") + s.Require().Contains(agentLicenseInspectionOutput, "license_type: standard") + s.Require().Contains(agentLicenseInspectionOutput, "sub: test-customer-id-123") + s.Require().Contains(agentLicenseInspectionOutput, "product: Bacalhau") + s.Require().Contains(agentLicenseInspectionOutput, "license_version: v1") + s.Require().Contains(agentLicenseInspectionOutput, "max_nodes: \"1\"") +} + +func TestAgentLicenseInspectWithMetadataSuite(t *testing.T) { + suite.Run(t, NewAgentLicenseInspectWithMetadataSuite()) +} diff --git a/test_integration/15_agent_license_inspect_with_no_license_suite_test.go b/test_integration/15_agent_license_inspect_with_no_license_suite_test.go new file mode 100644 index 0000000000..2c0486c663 --- /dev/null +++ b/test_integration/15_agent_license_inspect_with_no_license_suite_test.go @@ -0,0 +1,53 @@ +package test_integration + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" +) + +type AgentLicenseInspectWithNoLicenseSuite struct { + BaseDockerComposeTestSuite +} + +func NewAgentLicenseInspectWithNoLicenseSuite() *AgentLicenseInspectWithNoLicenseSuite { + s := &AgentLicenseInspectWithNoLicenseSuite{} + s.GlobalRunIdentifier = globalTestExecutionId + s.SuiteRunIdentifier = strings.ToLower(strings.Split(uuid.New().String(), "-")[0]) + return s +} + +func (s *AgentLicenseInspectWithNoLicenseSuite) SetupSuite() { + rawDockerComposeFilePath := "./common_assets/docker_compose_files/orchestrator-node-with-custom-start-command.yml" + s.Context, s.Cancel = context.WithCancel(context.Background()) + + orchestratorConfigFile := s.commonAssets("nodes_configs/15_orchestrator_config_with_no_license.yaml") + orchestratorStartCommand := fmt.Sprintf("bacalhau serve --config=%s", orchestratorConfigFile) + extraRenderingData := map[string]interface{}{ + "OrchestratorStartCommand": orchestratorStartCommand, + } + s.BaseDockerComposeTestSuite.SetupSuite(rawDockerComposeFilePath, extraRenderingData) +} + +func (s *AgentLicenseInspectWithNoLicenseSuite) TearDownSuite() { + s.T().Log("Tearing down [Test Suite] in AgentLicenseInspectWithNoLicenseSuite...") + s.BaseDockerComposeTestSuite.TearDownSuite() +} + +func (s *AgentLicenseInspectWithNoLicenseSuite) TestValidateRemoteLicenseWithNoLicense() { + _, err := s.executeCommandInDefaultJumpbox( + []string{ + "bacalhau", "agent", "license", "inspect", + }, + ) + + s.Require().ErrorContains(err, "Node license not configured") +} + +func TestAgentLicenseInspectWithNoLicenseSuite(t *testing.T) { + suite.Run(t, NewAgentLicenseInspectWithNoLicenseSuite()) +} diff --git a/test_integration/common_assets/licenses/test-license-with-metadata.json b/test_integration/common_assets/licenses/test-license-with-metadata.json new file mode 100644 index 0000000000..632c6b56cb --- /dev/null +++ b/test_integration/common_assets/licenses/test-license-with-metadata.json @@ -0,0 +1,3 @@ +{ + "license": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6IjJkNThjN2M5LWVjMjktNDVhNS1hNWNkLWNiOGY3ZmVlNjY3OCIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6eyJzb21lTWV0YWRhdGEiOiJ2YWx1ZU9mU29tZU1ldGFkYXRhIn0sImlhdCI6MTczNjg4OTY4MiwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODg5NjgyLCJqdGkiOiIyZDU4YzdjOS1lYzI5LTQ1YTUtYTVjZC1jYjhmN2ZlZTY2NzgifQ.LDjEcSkGBHT6cHazgYYmviX6jxUPcEzVrkiyJ1QCgwdAswWusC2gWE-H7vu6X4rFFYV8hjycS2oJjaVLm4hLyGNvHPzRedIshGWM5j4GxoQ-p7ulf1HQErVMj5xzJzoyM0IwXN4Vb6h6AxNwYoey948Bduk--DeYBbMVwQAXyZeyb_A1jZeR3JLf1lQhoe6-cjmTnVMCNyzisZqHGYWpXHDYQcqSOm3FvPrBPsP4bVCZSU0pGQBu8lb9A3KhJRobvqNF4YseSz7fFkpuRR3sI7p4zthO6aEk7sXKF0LBU9G1AEdCn5S0gB-7_uFUuH_JQi8bhvXeWvC1dqdQLBzYnA" +} \ No newline at end of file diff --git a/test_integration/common_assets/nodes_configs/13_orchestrator_config_with_license.yaml b/test_integration/common_assets/nodes_configs/13_orchestrator_config_with_license.yaml new file mode 100644 index 0000000000..31b06ae2cf --- /dev/null +++ b/test_integration/common_assets/nodes_configs/13_orchestrator_config_with_license.yaml @@ -0,0 +1,12 @@ +NameProvider: "uuid" +API: + Port: 1234 +Orchestrator: + Enabled: true + Auth: + Token: "i_am_very_secret_token" + License: + LocalPath: "/bacalhau_integration_tests/common_assets/licenses/test-license.json" +Labels: + label1: label1Value + label2: label2Value diff --git a/test_integration/common_assets/nodes_configs/14_orchestrator_config_with_license_with_metadata.yaml b/test_integration/common_assets/nodes_configs/14_orchestrator_config_with_license_with_metadata.yaml new file mode 100644 index 0000000000..9613df39af --- /dev/null +++ b/test_integration/common_assets/nodes_configs/14_orchestrator_config_with_license_with_metadata.yaml @@ -0,0 +1,12 @@ +NameProvider: "uuid" +API: + Port: 1234 +Orchestrator: + Enabled: true + Auth: + Token: "i_am_very_secret_token" + License: + LocalPath: "/bacalhau_integration_tests/common_assets/licenses/test-license-with-metadata.json" +Labels: + label1: label1Value + label2: label2Value diff --git a/test_integration/common_assets/nodes_configs/15_orchestrator_config_with_no_license.yaml b/test_integration/common_assets/nodes_configs/15_orchestrator_config_with_no_license.yaml new file mode 100644 index 0000000000..ff3627b25d --- /dev/null +++ b/test_integration/common_assets/nodes_configs/15_orchestrator_config_with_no_license.yaml @@ -0,0 +1,10 @@ +NameProvider: "uuid" +API: + Port: 1234 +Orchestrator: + Enabled: true + Auth: + Token: "i_am_very_secret_token" +Labels: + label1: label1Value + label2: label2Value From dda3693275e2f8af44a4a88ea3392a2203fc2be0 Mon Sep 17 00:00:00 2001 From: jamlo Date: Thu, 16 Jan 2025 15:46:13 -0500 Subject: [PATCH 2/3] Fix cspell issues --- pkg/publicapi/endpoint/agent/endpoint_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/publicapi/endpoint/agent/endpoint_test.go b/pkg/publicapi/endpoint/agent/endpoint_test.go index 1cabfec751..aa10d9c3ae 100644 --- a/pkg/publicapi/endpoint/agent/endpoint_test.go +++ b/pkg/publicapi/endpoint/agent/endpoint_test.go @@ -19,6 +19,7 @@ import ( "github.com/bacalhau-project/bacalhau/pkg/publicapi/apimodels" ) +// cSpell:disable const validOfficialTestLicense = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6ImU2NmQxZjNhLWE4ZDgtNGQ1Ny04ZjE0LTAwNzIyODQ0YWZlMiIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6e30sImlhdCI6MTczNjg4MTYzOCwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODgxNjM4LCJqdGkiOiJlNjZkMWYzYS1hOGQ4LTRkNTctOGYxNC0wMDcyMjg0NGFmZTIifQ.U6qkWmki2wp3RbPdn8d0zzsy4FchZIyUDmJi2bJ4w4vhwJlJ0_F2_317v4iPzy9q69eJOKNaqj8P3xYaPbpiooFm15OdJ3ecbMy8bKvvWVj43stw6HNP_uoW-RlZnY2zTOQ9WhlOhjnUPPC-UXOcaMwxiLBwMo5n3Rs0W9uAQHGQIptGg0sKiZvIrMZZ3vww2PZ3wJDiDvznE2lPtI7jAbcFFKDlhY3UiXed2ihGTWvLW8Zwj4veCR4PAUoEDu-nfQDvlqNeAvABT-KrKY2M-d5T_WzK1WwXtHok9tG2OV5ybSZoxFDQW3iqiCg6TqMwCAa6C6MBXtLnv-NP1H9Ytg" // TestEndpointConfigRedactFields asserts that auth tokens in the config are redacted. From 1d3b71d3d0d9ac9678582380bd5890b4dcd1990e Mon Sep 17 00:00:00 2001 From: jamlo Date: Thu, 16 Jan 2025 15:49:47 -0500 Subject: [PATCH 3/3] FI=ix swagger generation --- pkg/swagger/docs.go | 137 +++++++++++++++++++++++++++++- pkg/swagger/swagger.json | 133 +++++++++++++++++++++++++++++ webui/lib/api/schema/swagger.json | 133 +++++++++++++++++++++++++++++ 3 files changed, 402 insertions(+), 1 deletion(-) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 0e67f77d4e..b7ca753e1e 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1,4 +1,4 @@ -// Code generated by swaggo/swag. DO NOT EDIT +// Package swagger Code generated by swaggo/swag. DO NOT EDIT package swagger import "github.com/swaggo/swag" @@ -113,6 +113,38 @@ const docTemplate = `{ } } }, + "/api/v1/agent/license": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Ops" + ], + "summary": "Returns the details of the current configured orchestrator license.", + "operationId": "agent/license", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/license.LicenseClaims" + } + }, + "404": { + "description": "Node license not configured", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, "/api/v1/agent/node": { "get": { "produces": [ @@ -1184,6 +1216,90 @@ const docTemplate = `{ } } }, + "jwt.NumericDate": { + "type": "object", + "properties": { + "time.Time": { + "type": "string" + } + } + }, + "license.LicenseClaims": { + "type": "object", + "properties": { + "aud": { + "description": "the ` + "`" + `aud` + "`" + ` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3", + "type": "array", + "items": { + "type": "string" + } + }, + "capabilities": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "customer_id": { + "type": "string" + }, + "exp": { + "description": "the ` + "`" + `exp` + "`" + ` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4", + "allOf": [ + { + "$ref": "#/definitions/jwt.NumericDate" + } + ] + }, + "iat": { + "description": "the ` + "`" + `iat` + "`" + ` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6", + "allOf": [ + { + "$ref": "#/definitions/jwt.NumericDate" + } + ] + }, + "iss": { + "description": "the ` + "`" + `iss` + "`" + ` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1", + "type": "string" + }, + "jti": { + "description": "the ` + "`" + `jti` + "`" + ` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7", + "type": "string" + }, + "license_id": { + "type": "string" + }, + "license_type": { + "type": "string" + }, + "license_version": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "nbf": { + "description": "the ` + "`" + `nbf` + "`" + ` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5", + "allOf": [ + { + "$ref": "#/definitions/jwt.NumericDate" + } + ] + }, + "product": { + "description": "Add your custom license claims here", + "type": "string" + }, + "sub": { + "description": "the ` + "`" + `sub` + "`" + ` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2", + "type": "string" + } + } + }, "models.AllocatedResources": { "type": "object", "properties": { @@ -2672,6 +2788,15 @@ const docTemplate = `{ } } }, + "types.License": { + "type": "object", + "properties": { + "LocalPath": { + "description": "LocalPath specifies the local license file path", + "type": "string" + } + } + }, "types.LocalPublisher": { "type": "object", "properties": { @@ -2764,6 +2889,14 @@ const docTemplate = `{ "description": "Host specifies the hostname or IP address on which the Orchestrator server listens for compute node connections.", "type": "string" }, + "License": { + "description": "License specifies license configuration for orchestrator node", + "allOf": [ + { + "$ref": "#/definitions/types.License" + } + ] + }, "NodeManager": { "$ref": "#/definitions/types.NodeManager" }, @@ -3042,6 +3175,8 @@ var SwaggerInfo = &swag.Spec{ Description: "This page is the reference of the Bacalhau REST API. Project docs are available at https://docs.bacalhau.org/. Find more information about Bacalhau at https://github.com/bacalhau-project/bacalhau.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", } func init() { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 87579c4706..79098db407 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -109,6 +109,38 @@ } } }, + "/api/v1/agent/license": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Ops" + ], + "summary": "Returns the details of the current configured orchestrator license.", + "operationId": "agent/license", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/license.LicenseClaims" + } + }, + "404": { + "description": "Node license not configured", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, "/api/v1/agent/node": { "get": { "produces": [ @@ -1180,6 +1212,90 @@ } } }, + "jwt.NumericDate": { + "type": "object", + "properties": { + "time.Time": { + "type": "string" + } + } + }, + "license.LicenseClaims": { + "type": "object", + "properties": { + "aud": { + "description": "the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3", + "type": "array", + "items": { + "type": "string" + } + }, + "capabilities": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "customer_id": { + "type": "string" + }, + "exp": { + "description": "the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4", + "allOf": [ + { + "$ref": "#/definitions/jwt.NumericDate" + } + ] + }, + "iat": { + "description": "the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6", + "allOf": [ + { + "$ref": "#/definitions/jwt.NumericDate" + } + ] + }, + "iss": { + "description": "the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1", + "type": "string" + }, + "jti": { + "description": "the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7", + "type": "string" + }, + "license_id": { + "type": "string" + }, + "license_type": { + "type": "string" + }, + "license_version": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "nbf": { + "description": "the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5", + "allOf": [ + { + "$ref": "#/definitions/jwt.NumericDate" + } + ] + }, + "product": { + "description": "Add your custom license claims here", + "type": "string" + }, + "sub": { + "description": "the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2", + "type": "string" + } + } + }, "models.AllocatedResources": { "type": "object", "properties": { @@ -2668,6 +2784,15 @@ } } }, + "types.License": { + "type": "object", + "properties": { + "LocalPath": { + "description": "LocalPath specifies the local license file path", + "type": "string" + } + } + }, "types.LocalPublisher": { "type": "object", "properties": { @@ -2760,6 +2885,14 @@ "description": "Host specifies the hostname or IP address on which the Orchestrator server listens for compute node connections.", "type": "string" }, + "License": { + "description": "License specifies license configuration for orchestrator node", + "allOf": [ + { + "$ref": "#/definitions/types.License" + } + ] + }, "NodeManager": { "$ref": "#/definitions/types.NodeManager" }, diff --git a/webui/lib/api/schema/swagger.json b/webui/lib/api/schema/swagger.json index 87579c4706..79098db407 100644 --- a/webui/lib/api/schema/swagger.json +++ b/webui/lib/api/schema/swagger.json @@ -109,6 +109,38 @@ } } }, + "/api/v1/agent/license": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Ops" + ], + "summary": "Returns the details of the current configured orchestrator license.", + "operationId": "agent/license", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/license.LicenseClaims" + } + }, + "404": { + "description": "Node license not configured", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, "/api/v1/agent/node": { "get": { "produces": [ @@ -1180,6 +1212,90 @@ } } }, + "jwt.NumericDate": { + "type": "object", + "properties": { + "time.Time": { + "type": "string" + } + } + }, + "license.LicenseClaims": { + "type": "object", + "properties": { + "aud": { + "description": "the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3", + "type": "array", + "items": { + "type": "string" + } + }, + "capabilities": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "customer_id": { + "type": "string" + }, + "exp": { + "description": "the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4", + "allOf": [ + { + "$ref": "#/definitions/jwt.NumericDate" + } + ] + }, + "iat": { + "description": "the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6", + "allOf": [ + { + "$ref": "#/definitions/jwt.NumericDate" + } + ] + }, + "iss": { + "description": "the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1", + "type": "string" + }, + "jti": { + "description": "the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7", + "type": "string" + }, + "license_id": { + "type": "string" + }, + "license_type": { + "type": "string" + }, + "license_version": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "nbf": { + "description": "the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5", + "allOf": [ + { + "$ref": "#/definitions/jwt.NumericDate" + } + ] + }, + "product": { + "description": "Add your custom license claims here", + "type": "string" + }, + "sub": { + "description": "the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2", + "type": "string" + } + } + }, "models.AllocatedResources": { "type": "object", "properties": { @@ -2668,6 +2784,15 @@ } } }, + "types.License": { + "type": "object", + "properties": { + "LocalPath": { + "description": "LocalPath specifies the local license file path", + "type": "string" + } + } + }, "types.LocalPublisher": { "type": "object", "properties": { @@ -2760,6 +2885,14 @@ "description": "Host specifies the hostname or IP address on which the Orchestrator server listens for compute node connections.", "type": "string" }, + "License": { + "description": "License specifies license configuration for orchestrator node", + "allOf": [ + { + "$ref": "#/definitions/types.License" + } + ] + }, "NodeManager": { "$ref": "#/definitions/types.NodeManager" },