Skip to content

Commit f67df8d

Browse files
committed
[OCTRL-1090] created kubernetes client to interact with cluster
1 parent 81a1fca commit f67df8d

3 files changed

Lines changed: 389 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* === This file is part of ALICE O² ===
3+
*
4+
* Copyright 2026 CERN and copyright holders of ALICE O².
5+
* Author: Michal Tichak <michal.tichak@cern.ch>
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*
20+
* In applying this license CERN does not waive the privileges and
21+
* immunities granted to it by virtue of its status as an
22+
* Intergovernmental Organization or submit itself to any jurisdiction.
23+
*/
24+
25+
package client
26+
27+
import (
28+
"context"
29+
"fmt"
30+
31+
"github.com/AliceO2Group/Control/operator/api/v1alpha1"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/runtime"
34+
"k8s.io/apimachinery/pkg/types"
35+
"k8s.io/apimachinery/pkg/watch"
36+
"k8s.io/client-go/rest"
37+
"k8s.io/client-go/tools/clientcmd"
38+
crClient "sigs.k8s.io/controller-runtime/pkg/client"
39+
)
40+
41+
type Client struct {
42+
client crClient.WithWatch
43+
namespace string
44+
}
45+
46+
func New(kubeconfigPath, namespace string) (*Client, error) {
47+
config, err := buildConfig(kubeconfigPath)
48+
if err != nil {
49+
return nil, fmt.Errorf("building kubeconfig: %w", err)
50+
}
51+
return NewFromConfig(config, namespace)
52+
}
53+
54+
func NewFromConfig(config *rest.Config, namespace string) (*Client, error) {
55+
scheme := runtime.NewScheme()
56+
if err := v1alpha1.AddToScheme(scheme); err != nil {
57+
return nil, fmt.Errorf("registering v1alpha1 scheme: %w", err)
58+
}
59+
60+
c, err := crClient.NewWithWatch(config, crClient.Options{Scheme: scheme})
61+
if err != nil {
62+
return nil, fmt.Errorf("creating kubernetes client: %w", err)
63+
}
64+
65+
return &Client{client: c, namespace: namespace}, nil
66+
}
67+
68+
func buildConfig(kubeconfigPath string) (*rest.Config, error) {
69+
if kubeconfigPath != "" {
70+
return clientcmd.BuildConfigFromFlags("", kubeconfigPath)
71+
}
72+
if config, err := rest.InClusterConfig(); err == nil {
73+
return config, nil
74+
}
75+
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
76+
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, nil).ClientConfig()
77+
}
78+
79+
func (c *Client) CreateTask(ctx context.Context, task *v1alpha1.Task) error {
80+
task.Namespace = c.namespace
81+
return c.client.Create(ctx, task)
82+
}
83+
84+
func (c *Client) GetTask(ctx context.Context, name string) (*v1alpha1.Task, error) {
85+
task := &v1alpha1.Task{}
86+
err := c.client.Get(ctx, types.NamespacedName{Name: name, Namespace: c.namespace}, task)
87+
return task, err
88+
}
89+
90+
func (c *Client) UpdateTask(ctx context.Context, task *v1alpha1.Task) error {
91+
task.Namespace = c.namespace
92+
return c.client.Update(ctx, task)
93+
}
94+
95+
func (c *Client) DeleteTask(ctx context.Context, name string) error {
96+
return c.client.Delete(ctx, &v1alpha1.Task{
97+
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: c.namespace},
98+
})
99+
}
100+
101+
// WatchTasks returns a watcher for all Task resources in the namespace.
102+
// Each event on ResultChan() carries a *v1alpha1.Task as event.Object.
103+
func (c *Client) WatchTasks(ctx context.Context) (watch.Interface, error) {
104+
return c.client.Watch(ctx, &v1alpha1.TaskList{}, crClient.InNamespace(c.namespace))
105+
}
106+
107+
func (c *Client) CreateEnvironment(ctx context.Context, env *v1alpha1.Environment) error {
108+
env.Namespace = c.namespace
109+
return c.client.Create(ctx, env)
110+
}
111+
112+
func (c *Client) GetEnvironment(ctx context.Context, name string) (*v1alpha1.Environment, error) {
113+
env := &v1alpha1.Environment{}
114+
err := c.client.Get(ctx, types.NamespacedName{Name: name, Namespace: c.namespace}, env)
115+
return env, err
116+
}
117+
118+
func (c *Client) UpdateEnvironment(ctx context.Context, env *v1alpha1.Environment) error {
119+
env.Namespace = c.namespace
120+
return c.client.Update(ctx, env)
121+
}
122+
123+
func (c *Client) DeleteEnvironment(ctx context.Context, name string) error {
124+
return c.client.Delete(ctx, &v1alpha1.Environment{
125+
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: c.namespace},
126+
})
127+
}
128+
129+
// WatchEnvironments returns a watcher for all Environment resources in the namespace.
130+
// Each event on ResultChan() carries a *v1alpha1.Environment as event.Object.
131+
func (c *Client) WatchEnvironments(ctx context.Context) (watch.Interface, error) {
132+
return c.client.Watch(ctx, &v1alpha1.EnvironmentList{}, crClient.InNamespace(c.namespace))
133+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* === This file is part of ALICE O² ===
3+
*
4+
* Copyright 2026 CERN and copyright holders of ALICE O².
5+
* Author: Michal Tichak <michal.tichak@cern.ch>
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*
20+
* In applying this license CERN does not waive the privileges and
21+
* immunities granted to it by virtue of its status as an
22+
* Intergovernmental Organization or submit itself to any jurisdiction.
23+
*/
24+
25+
package client_test
26+
27+
import (
28+
"context"
29+
30+
. "github.com/onsi/ginkgo/v2"
31+
. "github.com/onsi/gomega"
32+
v1 "k8s.io/api/core/v1"
33+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
k8swatch "k8s.io/apimachinery/pkg/watch"
35+
36+
aliecsv1alpha1 "github.com/AliceO2Group/Control/operator/api/v1alpha1"
37+
"github.com/AliceO2Group/Control/operator/pkg/client"
38+
)
39+
40+
var _ = Describe("Client", func() {
41+
var (
42+
ctx context.Context
43+
c *client.Client
44+
)
45+
46+
BeforeEach(func() {
47+
ctx = context.Background()
48+
var err error
49+
c, err = client.NewFromConfig(cfg, "default")
50+
Expect(err).NotTo(HaveOccurred())
51+
})
52+
53+
Describe("Task Create, Read, Update, Delete", func() {
54+
var task *aliecsv1alpha1.Task
55+
56+
BeforeEach(func() {
57+
task = &aliecsv1alpha1.Task{
58+
ObjectMeta: metav1.ObjectMeta{Name: "test-task"},
59+
Spec: aliecsv1alpha1.TaskSpec{State: "standby", Pod: v1.PodSpec{Containers: []v1.Container{}}},
60+
}
61+
Expect(c.CreateTask(ctx, task)).To(Succeed())
62+
})
63+
64+
AfterEach(func() {
65+
_ = c.DeleteTask(ctx, task.Name)
66+
})
67+
68+
It("gets a created task", func() {
69+
got, err := c.GetTask(ctx, "test-task")
70+
Expect(err).NotTo(HaveOccurred())
71+
Expect(got.Name).To(Equal("test-task"))
72+
Expect(got.Spec.State).To(Equal("standby"))
73+
})
74+
75+
It("updates a task", func() {
76+
got, err := c.GetTask(ctx, "test-task")
77+
Expect(err).NotTo(HaveOccurred())
78+
79+
got.Spec.State = "running"
80+
Expect(c.UpdateTask(ctx, got)).To(Succeed())
81+
82+
updated, err := c.GetTask(ctx, "test-task")
83+
Expect(err).NotTo(HaveOccurred())
84+
Expect(updated.Spec.State).To(Equal("running"))
85+
})
86+
87+
It("deletes a task", func() {
88+
Expect(c.DeleteTask(ctx, "test-task")).To(Succeed())
89+
90+
_, err := c.GetTask(ctx, "test-task")
91+
Expect(err).To(HaveOccurred())
92+
})
93+
})
94+
95+
Describe("Task Watch", func() {
96+
It("receives events for task changes", func() {
97+
watcher, err := c.WatchTasks(ctx)
98+
Expect(err).NotTo(HaveOccurred())
99+
defer watcher.Stop()
100+
101+
task := &aliecsv1alpha1.Task{
102+
ObjectMeta: metav1.ObjectMeta{Name: "watch-task"},
103+
Spec: aliecsv1alpha1.TaskSpec{State: "standby", Pod: v1.PodSpec{Containers: []v1.Container{}}},
104+
}
105+
Expect(c.CreateTask(ctx, task)).To(Succeed())
106+
defer c.DeleteTask(ctx, task.Name)
107+
108+
Eventually(watcher.ResultChan()).Should(Receive(Satisfy(func(e k8swatch.Event) bool {
109+
t, ok := e.Object.(*aliecsv1alpha1.Task)
110+
return ok && t.Name == "watch-task" && e.Type == k8swatch.Added
111+
})))
112+
})
113+
})
114+
115+
Describe("Environment Create, Read, Update, Delete", func() {
116+
var env *aliecsv1alpha1.Environment
117+
118+
BeforeEach(func() {
119+
env = &aliecsv1alpha1.Environment{
120+
ObjectMeta: metav1.ObjectMeta{Name: "test-env"},
121+
Spec: aliecsv1alpha1.EnvironmentSpec{
122+
State: "standby",
123+
Tasks: map[string][]aliecsv1alpha1.TaskDefinition{},
124+
},
125+
TaskTemplates: aliecsv1alpha1.TemplateSpecification{
126+
Tasks: map[string][]aliecsv1alpha1.TaskReference{},
127+
},
128+
}
129+
Expect(c.CreateEnvironment(ctx, env)).To(Succeed())
130+
})
131+
132+
AfterEach(func() {
133+
_ = c.DeleteEnvironment(ctx, env.Name)
134+
})
135+
136+
It("gets a created environment", func() {
137+
got, err := c.GetEnvironment(ctx, "test-env")
138+
Expect(err).NotTo(HaveOccurred())
139+
Expect(got.Name).To(Equal("test-env"))
140+
Expect(got.Spec.State).To(Equal("standby"))
141+
})
142+
143+
It("updates an environment", func() {
144+
got, err := c.GetEnvironment(ctx, "test-env")
145+
Expect(err).NotTo(HaveOccurred())
146+
147+
got.Spec.State = "running"
148+
Expect(c.UpdateEnvironment(ctx, got)).To(Succeed())
149+
150+
updated, err := c.GetEnvironment(ctx, "test-env")
151+
Expect(err).NotTo(HaveOccurred())
152+
Expect(updated.Spec.State).To(Equal("running"))
153+
})
154+
155+
It("deletes an environment", func() {
156+
Expect(c.DeleteEnvironment(ctx, "test-env")).To(Succeed())
157+
158+
_, err := c.GetEnvironment(ctx, "test-env")
159+
Expect(err).To(HaveOccurred())
160+
})
161+
})
162+
163+
Describe("Environment Watch", func() {
164+
It("receives events for environment changes", func() {
165+
watcher, err := c.WatchEnvironments(ctx)
166+
Expect(err).NotTo(HaveOccurred())
167+
defer watcher.Stop()
168+
169+
env := &aliecsv1alpha1.Environment{
170+
ObjectMeta: metav1.ObjectMeta{Name: "watch-env"},
171+
Spec: aliecsv1alpha1.EnvironmentSpec{
172+
State: "standby",
173+
Tasks: map[string][]aliecsv1alpha1.TaskDefinition{},
174+
},
175+
TaskTemplates: aliecsv1alpha1.TemplateSpecification{
176+
Tasks: map[string][]aliecsv1alpha1.TaskReference{},
177+
},
178+
}
179+
Expect(c.CreateEnvironment(ctx, env)).To(Succeed())
180+
defer c.DeleteEnvironment(ctx, env.Name)
181+
182+
Eventually(watcher.ResultChan()).Should(Receive(Satisfy(func(e k8swatch.Event) bool {
183+
ev, ok := e.Object.(*aliecsv1alpha1.Environment)
184+
return ok && ev.Name == "watch-env" && e.Type == k8swatch.Added
185+
})))
186+
})
187+
})
188+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* === This file is part of ALICE O² ===
3+
*
4+
* Copyright 2026 CERN and copyright holders of ALICE O².
5+
* Author: Michal Tichak <michal.tichak@cern.ch>
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*
20+
* In applying this license CERN does not waive the privileges and
21+
* immunities granted to it by virtue of its status as an
22+
* Intergovernmental Organization or submit itself to any jurisdiction.
23+
*/
24+
25+
package client_test
26+
27+
import (
28+
"path/filepath"
29+
"testing"
30+
31+
. "github.com/onsi/ginkgo/v2"
32+
. "github.com/onsi/gomega"
33+
34+
"k8s.io/client-go/rest"
35+
"sigs.k8s.io/controller-runtime/pkg/envtest"
36+
logf "sigs.k8s.io/controller-runtime/pkg/log"
37+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
38+
)
39+
40+
var (
41+
cfg *rest.Config
42+
testEnv *envtest.Environment
43+
)
44+
45+
func TestClient(t *testing.T) {
46+
RegisterFailHandler(Fail)
47+
RunSpecs(t, "Client Suite")
48+
}
49+
50+
var _ = BeforeSuite(func() {
51+
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
52+
53+
By("bootstrapping test environment")
54+
testEnv = &envtest.Environment{
55+
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
56+
ErrorIfCRDPathMissing: true,
57+
}
58+
59+
var err error
60+
cfg, err = testEnv.Start()
61+
Expect(err).NotTo(HaveOccurred())
62+
Expect(cfg).NotTo(BeNil())
63+
})
64+
65+
var _ = AfterSuite(func() {
66+
By("tearing down the test environment")
67+
Expect(testEnv.Stop()).To(Succeed())
68+
})

0 commit comments

Comments
 (0)