Skip to content
Merged
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
26 changes: 26 additions & 0 deletions api/v1alpha1/codercontrolplane_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ const (
CoderControlPlanePhaseReady = "Ready"
// CoderControlPlaneConditionLicenseApplied indicates whether the operator uploaded the configured license.
CoderControlPlaneConditionLicenseApplied = "LicenseApplied"

// CoderControlPlaneLicenseTierNone indicates no license is currently installed.
CoderControlPlaneLicenseTierNone = "none"
// CoderControlPlaneLicenseTierTrial indicates a trial license is currently installed.
CoderControlPlaneLicenseTierTrial = "trial"
// CoderControlPlaneLicenseTierEnterprise indicates an enterprise license is currently installed.
CoderControlPlaneLicenseTierEnterprise = "enterprise"
// CoderControlPlaneLicenseTierPremium indicates a premium license is currently installed.
CoderControlPlaneLicenseTierPremium = "premium"
// CoderControlPlaneLicenseTierUnknown indicates the controller could not determine the current license tier.
CoderControlPlaneLicenseTierUnknown = "unknown"

// CoderControlPlaneEntitlementUnknown indicates the controller could not determine a feature entitlement.
CoderControlPlaneEntitlementUnknown = "unknown"
)

// CoderControlPlaneSpec defines the desired state of a CoderControlPlane.
Expand Down Expand Up @@ -71,6 +85,18 @@ type CoderControlPlaneStatus struct {
// that LicenseLastApplied refers to.
// +optional
LicenseLastAppliedHash string `json:"licenseLastAppliedHash,omitempty"`
// LicenseTier is a best-effort classification of the currently applied license.
// Values: none, trial, enterprise, premium, unknown.
// +optional
LicenseTier string `json:"licenseTier,omitempty"`
// EntitlementsLastChecked is when the operator last queried coderd entitlements.
// +optional
EntitlementsLastChecked *metav1.Time `json:"entitlementsLastChecked,omitempty"`
// ExternalProvisionerDaemonsEntitlement is the entitlement value for feature
// "external_provisioner_daemons".
// Values: entitled, grace_period, not_entitled, unknown.
// +optional
ExternalProvisionerDaemonsEntitlement string `json:"externalProvisionerDaemonsEntitlement,omitempty"`
// Phase is a high-level readiness indicator.
Phase string `json:"phase,omitempty"`
// Conditions are Kubernetes-standard conditions for this resource.
Expand Down
3 changes: 3 additions & 0 deletions api/v1alpha1/coderprovisioner_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const (
CoderProvisionerConditionProvisionerKeyReady = "ProvisionerKeyReady"
// CoderProvisionerConditionProvisionerKeySecretReady indicates whether the provisioner key secret is populated.
CoderProvisionerConditionProvisionerKeySecretReady = "ProvisionerKeySecretReady"
// CoderProvisionerConditionExternalProvisionersEntitled indicates whether the
// referenced Coder deployment is entitled to run external provisioner daemons.
CoderProvisionerConditionExternalProvisionersEntitled = "ExternalProvisionersEntitled"
// CoderProvisionerConditionDeploymentReady indicates whether the provisioner deployment has ready replicas.
CoderProvisionerConditionDeploymentReady = "DeploymentReady"

Expand Down
4 changes: 4 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions config/crd/bases/coder.com_codercontrolplanes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,17 @@ spec:
- type
type: object
type: array
entitlementsLastChecked:
description: EntitlementsLastChecked is when the operator last queried
coderd entitlements.
format: date-time
type: string
externalProvisionerDaemonsEntitlement:
description: |-
ExternalProvisionerDaemonsEntitlement is the entitlement value for feature
"external_provisioner_daemons".
Values: entitled, grace_period, not_entitled, unknown.
type: string
licenseLastApplied:
description: |-
LicenseLastApplied is the timestamp of the most recent successful
Expand All @@ -356,6 +367,11 @@ spec:
LicenseLastAppliedHash is the SHA-256 hex hash of the trimmed license JWT
that LicenseLastApplied refers to.
type: string
licenseTier:
description: |-
LicenseTier is a best-effort classification of the currently applied license.
Values: none, trial, enterprise, premium, unknown.
type: string
observedGeneration:
description: ObservedGeneration tracks the spec generation this status
reflects.
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/api/codercontrolplane.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
| `operatorAccessReady` | boolean | OperatorAccessReady reports whether operator API access bootstrap succeeded. |
| `licenseLastApplied` | [Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#time-v1-meta) | LicenseLastApplied is the timestamp of the most recent successful operator-managed license upload. |
| `licenseLastAppliedHash` | string | LicenseLastAppliedHash is the SHA-256 hex hash of the trimmed license JWT that LicenseLastApplied refers to. |
| `licenseTier` | string | LicenseTier is a best-effort classification of the currently applied license. Values: none, trial, enterprise, premium, unknown. |
| `entitlementsLastChecked` | [Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#time-v1-meta) | EntitlementsLastChecked is when the operator last queried coderd entitlements. |
| `externalProvisionerDaemonsEntitlement` | string | ExternalProvisionerDaemonsEntitlement is the entitlement value for feature "external_provisioner_daemons". Values: entitled, grace_period, not_entitled, unknown. |
| `phase` | string | Phase is a high-level readiness indicator. |
| `conditions` | [Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array | Conditions are Kubernetes-standard conditions for this resource. |

Expand Down
1 change: 1 addition & 0 deletions internal/app/controllerapp/controllerapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func SetupControllers(mgr manager.Manager) error {
Scheme: managerScheme,
OperatorAccessProvisioner: coderbootstrap.NewPostgresOperatorAccessProvisioner(),
LicenseUploader: controller.NewSDKLicenseUploader(),
EntitlementsInspector: controller.NewSDKEntitlementsInspector(),
}
if err := reconciler.SetupWithManager(mgr); err != nil {
return fmt.Errorf("unable to create controller: %w", err)
Expand Down
1 change: 1 addition & 0 deletions internal/coderbootstrap/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type Client interface {
EnsureWorkspaceProxy(context.Context, RegisterWorkspaceProxyRequest) (RegisterWorkspaceProxyResponse, error)
EnsureProvisionerKey(context.Context, EnsureProvisionerKeyRequest) (EnsureProvisionerKeyResponse, error)
DeleteProvisionerKey(ctx context.Context, coderURL, sessionToken, orgName, keyName string) error
Entitlements(ctx context.Context, coderURL, sessionToken string) (codersdk.Entitlements, error)
}

// SDKClient uses codersdk to perform bootstrap operations.
Expand Down
27 changes: 27 additions & 0 deletions internal/coderbootstrap/provisionerkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,33 @@ func (c *SDKClient) DeleteProvisionerKey(ctx context.Context, coderURL, sessionT
return xerrors.Errorf("delete provisioner key %q: %w", keyName, err)
}

// Entitlements returns deployment entitlements for the given coderd instance.
func (c *SDKClient) Entitlements(ctx context.Context, coderURL, sessionToken string) (codersdk.Entitlements, error) {
if coderURL == "" {
return codersdk.Entitlements{}, xerrors.New("coder URL is required")
}
if sessionToken == "" {
return codersdk.Entitlements{}, xerrors.New("session token is required")
}

client, err := newAuthenticatedClient(coderURL, sessionToken)
if err != nil {
return codersdk.Entitlements{}, err
}

entitlements, err := withOptionalRateLimitBypass(ctx, func(requestCtx context.Context) (codersdk.Entitlements, error) {
return client.Entitlements(requestCtx)
})
if err != nil {
return codersdk.Entitlements{}, xerrors.Errorf("get entitlements: %w", err)
}
if entitlements.Features == nil {
return codersdk.Entitlements{}, xerrors.New("assertion failed: entitlements.features is nil")
}

return entitlements, nil
}

func validateProvisionerKeyInputs(coderURL, sessionToken, keyName string) error {
if coderURL == "" {
return xerrors.New("coder URL is required")
Expand Down
49 changes: 49 additions & 0 deletions internal/coderbootstrap/provisionerkeys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,55 @@ func TestEnsureProvisionerKey_ValidationErrors(t *testing.T) {
}
}

func TestEntitlements_Success(t *testing.T) {
t.Parallel()

requestCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "/api/v2/entitlements", r.URL.Path)
requestCount++
writeJSONResponse(t, w, http.StatusOK, map[string]any{
"features": map[string]any{
string(codersdk.FeatureExternalProvisionerDaemons): map[string]any{
"entitlement": string(codersdk.EntitlementEntitled),
"enabled": true,
},
},
"warnings": []string{},
"errors": []string{},
"has_license": true,
"trial": false,
"require_telemetry": false,
"refreshed_at": time.Now().UTC().Format(time.RFC3339),
})
}))
defer server.Close()

client := coderbootstrap.NewSDKClient()
entitlements, err := client.Entitlements(context.Background(), server.URL, "session-token")
require.NoError(t, err)
require.Equal(t, 1, requestCount)

feature, ok := entitlements.Features[codersdk.FeatureExternalProvisionerDaemons]
require.True(t, ok)
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
}

func TestEntitlements_ValidationErrors(t *testing.T) {
t.Parallel()

client := coderbootstrap.NewSDKClient()

_, err := client.Entitlements(context.Background(), "", "session-token")
require.Error(t, err)
require.Contains(t, err.Error(), "coder URL is required")

_, err = client.Entitlements(context.Background(), "https://coder.example.com", "")
require.Error(t, err)
require.Contains(t, err.Error(), "session token is required")
}

func TestDeleteProvisionerKey_Success(t *testing.T) {
t.Parallel()

Expand Down
Loading