6969 errSecretValueEmpty = errors .New ("secret value empty" )
7070)
7171
72- // LicenseUploader uploads Coder Enterprise license JWTs to a coderd instance.
72+ // LicenseUploader uploads and inspects Coder Enterprise licenses in a coderd instance.
7373type LicenseUploader interface {
7474 AddLicense (ctx context.Context , coderURL , sessionToken , licenseJWT string ) error
75+ HasAnyLicense (ctx context.Context , coderURL , sessionToken string ) (bool , error )
7576}
7677
7778// NewSDKLicenseUploader returns a LicenseUploader backed by codersdk.
@@ -82,19 +83,47 @@ func NewSDKLicenseUploader() LicenseUploader {
8283type sdkLicenseUploader struct {}
8384
8485func (u * sdkLicenseUploader ) AddLicense (ctx context.Context , coderURL , sessionToken , licenseJWT string ) error {
86+ if licenseJWT == "" {
87+ return fmt .Errorf ("assertion failed: license JWT must not be empty" )
88+ }
89+
90+ sdkClient , err := newSDKLicenseClient (coderURL , sessionToken )
91+ if err != nil {
92+ return err
93+ }
94+
95+ if _ , err := sdkClient .AddLicense (ctx , codersdk.AddLicenseRequest {License : licenseJWT }); err != nil {
96+ return fmt .Errorf ("upload coder license: %w" , err )
97+ }
98+
99+ return nil
100+ }
101+
102+ func (u * sdkLicenseUploader ) HasAnyLicense (ctx context.Context , coderURL , sessionToken string ) (bool , error ) {
103+ sdkClient , err := newSDKLicenseClient (coderURL , sessionToken )
104+ if err != nil {
105+ return false , err
106+ }
107+
108+ licenses , err := sdkClient .Licenses (ctx )
109+ if err != nil {
110+ return false , fmt .Errorf ("list coder licenses: %w" , err )
111+ }
112+
113+ return len (licenses ) > 0 , nil
114+ }
115+
116+ func newSDKLicenseClient (coderURL , sessionToken string ) (* codersdk.Client , error ) {
85117 if strings .TrimSpace (coderURL ) == "" {
86- return fmt .Errorf ("assertion failed: coder URL must not be empty" )
118+ return nil , fmt .Errorf ("assertion failed: coder URL must not be empty" )
87119 }
88120 if sessionToken == "" {
89- return fmt .Errorf ("assertion failed: session token must not be empty" )
90- }
91- if licenseJWT == "" {
92- return fmt .Errorf ("assertion failed: license JWT must not be empty" )
121+ return nil , fmt .Errorf ("assertion failed: session token must not be empty" )
93122 }
94123
95124 parsedURL , err := url .Parse (coderURL )
96125 if err != nil {
97- return fmt .Errorf ("parse coder URL: %w" , err )
126+ return nil , fmt .Errorf ("parse coder URL: %w" , err )
98127 }
99128
100129 sdkClient := codersdk .New (parsedURL )
@@ -104,24 +133,21 @@ func (u *sdkLicenseUploader) AddLicense(ctx context.Context, coderURL, sessionTo
104133 }
105134 defaultTransport , ok := http .DefaultTransport .(* http.Transport )
106135 if ! ok {
107- return fmt .Errorf ("assertion failed: http.DefaultTransport is not *http.Transport" )
136+ return nil , fmt .Errorf ("assertion failed: http.DefaultTransport is not *http.Transport" )
108137 }
109138 // Use a dedicated transport to avoid sharing http.DefaultTransport's
110139 // connection pool across parallel test servers.
111140 sdkClient .HTTPClient .Transport = defaultTransport .Clone ()
112141 sdkClient .HTTPClient .Timeout = licenseUploadRequestTimeout
113142
114- if _ , err := sdkClient .AddLicense (ctx , codersdk.AddLicenseRequest {License : licenseJWT }); err != nil {
115- return fmt .Errorf ("upload coder license: %w" , err )
116- }
117-
118- return nil
143+ return sdkClient , nil
119144}
120145
121146// CoderControlPlaneReconciler reconciles a CoderControlPlane object.
122147type CoderControlPlaneReconciler struct {
123148 client.Client
124- Scheme * runtime.Scheme
149+ APIReader client.Reader
150+ Scheme * runtime.Scheme
125151
126152 OperatorAccessProvisioner coderbootstrap.OperatorAccessProvisioner
127153 LicenseUploader LicenseUploader
@@ -565,17 +591,62 @@ func (r *CoderControlPlaneReconciler) reconcileLicense(
565591 }
566592
567593 if nextStatus .LicenseLastApplied != nil && nextStatus .LicenseLastAppliedHash == licenseHash {
568- if err := setControlPlaneCondition (
569- nextStatus ,
570- coderControlPlane .Generation ,
571- coderv1alpha1 .CoderControlPlaneConditionLicenseApplied ,
572- metav1 .ConditionTrue ,
573- licenseConditionReasonApplied ,
574- "Configured license is already applied." ,
575- ); err != nil {
576- return ctrl.Result {}, err
594+ hasAnyLicense , hasLicenseErr := r .LicenseUploader .HasAnyLicense (ctx , nextStatus .URL , operatorToken )
595+ if hasLicenseErr != nil {
596+ var sdkErr * codersdk.Error
597+ if errors .As (hasLicenseErr , & sdkErr ) {
598+ switch sdkErr .StatusCode () {
599+ case http .StatusNotFound :
600+ if err := setControlPlaneCondition (
601+ nextStatus ,
602+ coderControlPlane .Generation ,
603+ coderv1alpha1 .CoderControlPlaneConditionLicenseApplied ,
604+ metav1 .ConditionFalse ,
605+ licenseConditionReasonNotSupported ,
606+ "Control plane does not expose the Enterprise licenses API." ,
607+ ); err != nil {
608+ return ctrl.Result {}, err
609+ }
610+ return ctrl.Result {}, nil
611+ case http .StatusUnauthorized , http .StatusForbidden :
612+ if err := setControlPlaneCondition (
613+ nextStatus ,
614+ coderControlPlane .Generation ,
615+ coderv1alpha1 .CoderControlPlaneConditionLicenseApplied ,
616+ metav1 .ConditionFalse ,
617+ licenseConditionReasonForbidden ,
618+ "Operator token is not authorized to query configured licenses." ,
619+ ); err != nil {
620+ return ctrl.Result {}, err
621+ }
622+ return ctrl.Result {RequeueAfter : operatorAccessRetryInterval }, nil
623+ }
624+ }
625+ if err := setControlPlaneCondition (
626+ nextStatus ,
627+ coderControlPlane .Generation ,
628+ coderv1alpha1 .CoderControlPlaneConditionLicenseApplied ,
629+ metav1 .ConditionFalse ,
630+ licenseConditionReasonError ,
631+ "Failed to query existing licenses; retrying." ,
632+ ); err != nil {
633+ return ctrl.Result {}, err
634+ }
635+ return ctrl.Result {RequeueAfter : operatorAccessRetryInterval }, nil
636+ }
637+ if hasAnyLicense {
638+ if err := setControlPlaneCondition (
639+ nextStatus ,
640+ coderControlPlane .Generation ,
641+ coderv1alpha1 .CoderControlPlaneConditionLicenseApplied ,
642+ metav1 .ConditionTrue ,
643+ licenseConditionReasonApplied ,
644+ "Configured license is already applied." ,
645+ ); err != nil {
646+ return ctrl.Result {}, err
647+ }
648+ return ctrl.Result {}, nil
577649 }
578- return ctrl.Result {}, nil
579650 }
580651
581652 if err := r .LicenseUploader .AddLicense (ctx , nextStatus .URL , operatorToken , licenseJWT ); err != nil {
@@ -999,9 +1070,17 @@ func (r *CoderControlPlaneReconciler) reconcileStatus(
9991070 return fmt .Errorf ("assertion failed: coder control plane namespaced name must not be empty" )
10001071 }
10011072
1073+ statusReader := r .APIReader
1074+ if statusReader == nil {
1075+ statusReader = r .Client
1076+ }
1077+ if statusReader == nil {
1078+ return fmt .Errorf ("assertion failed: status reader must not be nil" )
1079+ }
1080+
10021081 if err := retry .RetryOnConflict (retry .DefaultRetry , func () error {
10031082 latest := & coderv1alpha1.CoderControlPlane {}
1004- if err := r .Get (ctx , namespacedName , latest ); err != nil {
1083+ if err := statusReader .Get (ctx , namespacedName , latest ); err != nil {
10051084 return err
10061085 }
10071086 if latest .Name != namespacedName .Name || latest .Namespace != namespacedName .Namespace {
0 commit comments