Skip to content

Commit

Permalink
Add Agent license inspect command
Browse files Browse the repository at this point in the history
  • Loading branch information
jamlo committed Jan 16, 2025
1 parent bba0b6b commit 1c78637
Show file tree
Hide file tree
Showing 17 changed files with 684 additions and 0 deletions.
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

0 comments on commit 1c78637

Please sign in to comment.