Skip to content

Commit 1bd2bc9

Browse files
author
Francesco Francomano
committed
Add Console Client and implement first company existance logic
1 parent f9d083f commit 1bd2bc9

File tree

7 files changed

+336
-1
lines changed

7 files changed

+336
-1
lines changed

api/core/v1alpha1/company_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type CompanySpec struct {
4040
type CompanyStatus struct {
4141
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
4242
// Important: Run "make" to regenerate code after modifying this file
43+
CompanyID string `json:"companyID,omitempty"`
4344
}
4445

4546
// +kubebuilder:object:root=true

deployments/operator/crds/core.mia-platform.eu_companies.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ spec:
6363
type: object
6464
status:
6565
description: CompanyStatus defines the observed state of Company.
66+
properties:
67+
companyID:
68+
description: |-
69+
INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
70+
Important: Run "make" to regenerate code after modifying this file
71+
type: string
6672
type: object
6773
type: object
6874
served: true

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/go-logr/logr v1.4.2
77
github.com/onsi/ginkgo/v2 v2.22.0
88
github.com/onsi/gomega v1.36.1
9+
k8s.io/api v0.33.0
910
k8s.io/apimachinery v0.33.0
1011
k8s.io/client-go v0.33.0
1112
sigs.k8s.io/controller-runtime v0.21.0
@@ -82,7 +83,6 @@ require (
8283
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
8384
gopkg.in/inf.v0 v0.9.1 // indirect
8485
gopkg.in/yaml.v3 v3.0.1 // indirect
85-
k8s.io/api v0.33.0 // indirect
8686
k8s.io/apiextensions-apiserver v0.33.0 // indirect
8787
k8s.io/apiserver v0.33.0 // indirect
8888
k8s.io/component-base v0.33.0 // indirect

pkg/company-controller/company_controller.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ import (
2020
"context"
2121

2222
"github.com/go-logr/logr"
23+
v1 "k8s.io/api/core/v1"
2324
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/types"
2426
ctrl "sigs.k8s.io/controller-runtime"
2527
"sigs.k8s.io/controller-runtime/pkg/client"
2628
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2729

2830
corev1alpha1 "github.com/mia-platform/console-operator/api/core/v1alpha1"
31+
consoleclient "github.com/mia-platform/console-operator/pkg/console-client"
2932
)
3033

3134
// CompanyReconciler reconciles a Company object
@@ -73,6 +76,52 @@ func (r *CompanyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
7376
return ctrl.Result{}, nil
7477
}
7578

79+
companyName := company.Spec.Name
80+
companyConsoleRef := company.Spec.ConsoleRef
81+
var companyConsole corev1alpha1.Console
82+
if err := r.Get(ctx, types.NamespacedName{
83+
Name: companyConsoleRef.Name,
84+
Namespace: companyConsoleRef.Namespace,
85+
}, &companyConsole); err != nil {
86+
log.Error(err, "unable to fetch Console for Company", "consoleRef", companyConsoleRef)
87+
return ctrl.Result{}, client.IgnoreNotFound(err)
88+
}
89+
90+
var clientIdSecret v1.Secret
91+
if err := r.Get(ctx, types.NamespacedName{Name: companyConsole.Spec.ClientID.SecretRef, Namespace: companyConsole.Namespace}, &clientIdSecret); err != nil {
92+
log.Error(err, "unable to fetch ClientID Secret for Company")
93+
return ctrl.Result{}, err
94+
}
95+
96+
var clientKeySecret v1.Secret
97+
if err := r.Get(ctx, types.NamespacedName{Name: companyConsole.Spec.ClientSecret.SecretRef, Namespace: companyConsole.Namespace}, &clientKeySecret); err != nil {
98+
log.Error(err, "unable to fetch ClientKey Secret for Company")
99+
return ctrl.Result{}, err
100+
}
101+
102+
consoleClientConfig := consoleclient.ClientConfig{
103+
BaseURL: companyConsole.Spec.URL,
104+
ClientID: string(clientIdSecret.Data[companyConsole.Spec.ClientID.KeyRef]),
105+
ClientSecret: string(clientKeySecret.Data[companyConsole.Spec.ClientSecret.KeyRef]),
106+
}
107+
108+
consoleClient, err := consoleclient.NewClient(consoleClientConfig, ctx)
109+
if err != nil {
110+
log.Error(err, "failed to create Console client")
111+
return ctrl.Result{}, err
112+
}
113+
114+
exists, err := consoleClient.CompanyExists(ctx, companyName)
115+
if err != nil {
116+
log.Error(err, "failed to check if company exists in Console")
117+
return ctrl.Result{}, err
118+
}
119+
120+
if exists {
121+
log.Info("Company already exists", "companyName", companyName)
122+
return ctrl.Result{}, nil
123+
}
124+
76125
return ctrl.Result{}, nil
77126
}
78127

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Copyright 2016-2025 Mia-Platform
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package consoleclient
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"encoding/json"
21+
"fmt"
22+
"io"
23+
"net/http"
24+
"net/url"
25+
"strings"
26+
"time"
27+
)
28+
29+
func NewClient(config ClientConfig, ctx context.Context) (*Client, error) {
30+
if config.BaseURL == "" {
31+
return nil, fmt.Errorf("base URL is required")
32+
}
33+
if config.ClientID == "" {
34+
return nil, fmt.Errorf("client ID is required")
35+
}
36+
if config.ClientSecret == "" {
37+
return nil, fmt.Errorf("client secret is required")
38+
}
39+
40+
if _, err := url.Parse(config.BaseURL); err != nil {
41+
return nil, fmt.Errorf("invalid base URL: %w", err)
42+
}
43+
44+
timeout := config.Timeout
45+
if timeout == 0 {
46+
timeout = 30 * time.Second
47+
}
48+
49+
return &Client{
50+
baseURL: config.BaseURL,
51+
clientID: config.ClientID,
52+
clientSecret: config.ClientSecret,
53+
httpClient: &http.Client{
54+
Timeout: timeout,
55+
},
56+
ctx: ctx,
57+
}, nil
58+
}
59+
60+
// GetToken obtains a bearer token using client credentials flow
61+
func (c *Client) GetToken(ctx context.Context) error {
62+
// Check if token is still valid
63+
if c.token != "" && time.Now().Before(c.tokenExpiry) {
64+
return nil
65+
}
66+
67+
// Prepare form data for client credentials grant
68+
data := url.Values{}
69+
data.Set("grant_type", "client_credentials")
70+
71+
// Create token request
72+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/m2m/oauth/token", strings.NewReader(data.Encode()))
73+
if err != nil {
74+
return fmt.Errorf("failed to create token request: %w", err)
75+
}
76+
77+
// Set basic auth for client credentials
78+
req.SetBasicAuth(c.clientID, c.clientSecret)
79+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
80+
req.Header.Set("Accept", "application/json")
81+
82+
// Execute token request
83+
resp, err := c.httpClient.Do(req)
84+
if err != nil {
85+
return fmt.Errorf("failed to execute token request: %w", err)
86+
}
87+
defer resp.Body.Close()
88+
89+
if resp.StatusCode != http.StatusOK {
90+
return fmt.Errorf("token request failed with status %d: %s", resp.StatusCode, resp.Status)
91+
}
92+
93+
// Parse token response
94+
var tokenResp TokenResponse
95+
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
96+
return fmt.Errorf("failed to decode token response: %w", err)
97+
}
98+
99+
// Store token and calculate expiry
100+
c.token = tokenResp.AccessToken
101+
c.tokenExpiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
102+
103+
return nil
104+
}
105+
106+
func (c *Client) Get(ctx context.Context, endpoint string) (*http.Response, error) {
107+
return c.doRequest(ctx, http.MethodGet, endpoint, nil)
108+
}
109+
110+
func (c *Client) Post(ctx context.Context, endpoint string, body interface{}) (*http.Response, error) {
111+
return c.doRequest(ctx, http.MethodPost, endpoint, body)
112+
}
113+
114+
func (c *Client) Put(ctx context.Context, endpoint string, body interface{}) (*http.Response, error) {
115+
return c.doRequest(ctx, http.MethodPut, endpoint, body)
116+
}
117+
118+
func (c *Client) Delete(ctx context.Context, endpoint string) (*http.Response, error) {
119+
return c.doRequest(ctx, http.MethodDelete, endpoint, nil)
120+
}
121+
122+
func (c *Client) doRequest(ctx context.Context, method, endpoint string, body interface{}) (*http.Response, error) {
123+
// Ensure we have a valid token before making the request
124+
if err := c.GetToken(ctx); err != nil {
125+
return nil, fmt.Errorf("failed to get token: %w", err)
126+
}
127+
128+
fullURL := c.baseURL + endpoint
129+
130+
var reqBody io.Reader
131+
if body != nil {
132+
jsonBody, err := json.Marshal(body)
133+
if err != nil {
134+
return nil, fmt.Errorf("failed to marshal request body: %w", err)
135+
}
136+
reqBody = bytes.NewBuffer(jsonBody)
137+
}
138+
139+
req, err := http.NewRequestWithContext(ctx, method, fullURL, reqBody)
140+
if err != nil {
141+
return nil, fmt.Errorf("failed to create request: %w", err)
142+
}
143+
144+
// Add Bearer token authentication
145+
req.Header.Set("Authorization", "Bearer "+c.token)
146+
147+
// Set content type for requests with body
148+
if body != nil {
149+
req.Header.Set("Content-Type", "application/json")
150+
}
151+
152+
// Set accept header
153+
req.Header.Set("Accept", "application/json")
154+
155+
resp, err := c.httpClient.Do(req)
156+
if err != nil {
157+
return nil, fmt.Errorf("failed to execute request: %w", err)
158+
}
159+
160+
return resp, nil
161+
}
162+
163+
// GetJSON is a convenience method that performs GET and unmarshals JSON response
164+
func (c *Client) GetJSON(ctx context.Context, endpoint string, result interface{}) error {
165+
resp, err := c.Get(ctx, endpoint)
166+
if err != nil {
167+
return err
168+
}
169+
defer resp.Body.Close()
170+
171+
if resp.StatusCode >= 400 {
172+
return fmt.Errorf("request failed with status %d: %s", resp.StatusCode, resp.Status)
173+
}
174+
175+
return json.NewDecoder(resp.Body).Decode(result)
176+
}
177+
178+
func (c *Client) CompanyExists(ctx context.Context, companyName string) (bool, error) {
179+
var companies []ConsoleCompany
180+
181+
err := c.GetJSON(ctx, "/api/backend/tenants", &companies)
182+
if err != nil {
183+
return false, fmt.Errorf("failed to get companies: %w", err)
184+
}
185+
186+
for _, company := range companies {
187+
if company.Name == companyName {
188+
return true, nil
189+
}
190+
}
191+
192+
return false, nil
193+
}
194+
195+
// PostJSON is a convenience method that performs POST and unmarshals JSON response
196+
func (c *Client) PostJSON(ctx context.Context, endpoint string, body interface{}, result interface{}) error {
197+
resp, err := c.Post(ctx, endpoint, body)
198+
if err != nil {
199+
return err
200+
}
201+
defer resp.Body.Close()
202+
203+
if resp.StatusCode >= 400 {
204+
return fmt.Errorf("request failed with status %d: %s", resp.StatusCode, resp.Status)
205+
}
206+
207+
if result != nil {
208+
return json.NewDecoder(resp.Body).Decode(result)
209+
}
210+
211+
return nil
212+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2016-2025 Mia-Platform
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package consoleclient
16+
17+
import (
18+
"context"
19+
"net/http"
20+
"time"
21+
)
22+
23+
type Client struct {
24+
baseURL string
25+
clientID string
26+
clientSecret string
27+
httpClient *http.Client
28+
ctx context.Context
29+
token string
30+
tokenExpiry time.Time
31+
}
32+
33+
type ClientConfig struct {
34+
BaseURL string
35+
ClientID string
36+
ClientSecret string
37+
Timeout time.Duration
38+
}
39+
40+
type TokenResponse struct {
41+
AccessToken string `json:"access_token"`
42+
TokenType string `json:"token_type"`
43+
ExpiresIn int `json:"expires_in"`
44+
Scope string `json:"scope,omitempty"`
45+
}
46+
47+
type ConsoleCompany struct {
48+
ID string `json:"id"`
49+
Name string `json:"name"`
50+
Description string `json:"description,omitempty"`
51+
// Add other fields as needed based on your API response
52+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2016-2025 Mia-Platform
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package consoleclient

0 commit comments

Comments
 (0)