Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Agent license inspect command #4813

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions cmd/cli/agent/license/inspect.go
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 15 additions & 0 deletions cmd/cli/agent/license/root.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions cmd/cli/agent/root.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -17,5 +18,6 @@ func NewCmd() *cobra.Command {
cmd.AddCommand(NewNodeCmd())
cmd.AddCommand(NewVersionCmd())
cmd.AddCommand(NewConfigCmd())
cmd.AddCommand(license.NewAgentLicenseRootCmd())
return cmd
}
1 change: 1 addition & 0 deletions pkg/config/types/generated_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions pkg/config/types/generated_descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
7 changes: 7 additions & 0 deletions pkg/config/types/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"`
}
6 changes: 6 additions & 0 deletions pkg/publicapi/apimodels/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -38,3 +39,8 @@ type GetAgentConfigResponse struct {
BaseGetResponse
Config types.Bacalhau `json:"config"`
}

type GetAgentLicenseResponse struct {
BaseGetResponse
*license.LicenseClaims
}
7 changes: 7 additions & 0 deletions pkg/publicapi/client/v2/api_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
51 changes: 51 additions & 0 deletions pkg/publicapi/endpoint/agent/endpoint.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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,
})
}
Loading
Loading