From 8a6e5bf424245e1af9a6e3f0e576619c1b67a244 Mon Sep 17 00:00:00 2001 From: alcounit Date: Wed, 14 Jan 2026 11:23:15 -0600 Subject: [PATCH 1/4] Makefile update --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 846386d..9ff4d6f 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ EXTRA_TAGS ?= PLATFORM ?= linux/amd64 CONTAINER_TOOL ?= docker -.PHONY: all generate deepcopy client lister informer manifests install-tools verify clean fmt vet tidy docker-build docker-push deploy install help show-vars +.PHONY: all generate deepcopy client lister informer manifests install-tools verify clean fmt vet tidy test docker-build docker-push deploy install help show-vars all: generate manifests @@ -44,6 +44,9 @@ vet: tidy: go mod tidy +test: + go test -race -count=1 -cover ./... + deepcopy: @$(CONTROLLER_GEN) \ object:headerFile=$(BOILERPLATE) \ @@ -87,7 +90,7 @@ manifests: verify: @git diff --exit-code || (echo "Generated code is out of date. Run 'make generate'." && exit 1) -docker-build: manifests generate tidy fmt vet +docker-build: manifests generate tidy fmt vet test $(CONTAINER_TOOL) buildx build \ --platform $(PLATFORM) \ -t $(IMAGE_NAME):$(VERSION) \ From 78cd21ea0b3abb76935dc62cb83634783b719e4d Mon Sep 17 00:00:00 2001 From: alcounit Date: Tue, 20 Jan 2026 21:51:18 -0600 Subject: [PATCH 2/4] Add changes for https://github.com/alcounit/selenosis/issues/55 --- controllers/browser/browser_reconciler.go | 109 +++++++++++- .../browser/browser_reconciler_test.go | 165 +++++++++++++++++- 2 files changed, 265 insertions(+), 9 deletions(-) diff --git a/controllers/browser/browser_reconciler.go b/controllers/browser/browser_reconciler.go index 140bc54..f7de884 100644 --- a/controllers/browser/browser_reconciler.go +++ b/controllers/browser/browser_reconciler.go @@ -2,6 +2,7 @@ package browser import ( "context" + "encoding/json" "fmt" "strconv" "time" @@ -32,8 +33,19 @@ const ( browserContainerName = "browser" sidecarContainerName = "seleniferous" + + selenosisOptionsAnnotationKey = "selenosis.io/options" ) +type SelenosisOptions struct { + Labels map[string]string `json:"labels,omitempty"` + Containers map[string]ContainerOption `json:"containers,omitempty"` +} + +type ContainerOption struct { + Env map[string]string `json:"env,omitempty"` +} + // +kubebuilder:rbac:groups=selenosis.io,resources=browsers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=selenosis.io,resources=browsers/status,verbs=get;update;patch // +kubebuilder:rbac:groups=selenosis.io,resources=browsers/finalizers,verbs=update @@ -360,8 +372,26 @@ func (r *BrowserReconciler) handleMissingPod(ctx context.Context, browser *brows return ctrl.Result{}, nil } + opts, err := parseSelenosisOptions(browser.Annotations) + if err != nil { + log.Error(err, "invalid selenosis options JSON") + if err := r.retryStatusUpdate(ctx, browser, func(b *browserv1.Browser) { + b.Status.Phase = corev1.PodFailed + b.Status.Reason = "InvalidSelenosisOptions" + b.Status.Message = err.Error() + }); err != nil { + log.Error(err, "Failed to update Browser status") + return ctrl.Result{}, err + } + + log.Info("Invalid selenosis options") + return ctrl.Result{}, nil + } + + log.Info("parsed selenosis options", "hasOptions", opts != nil) + // Create pod from template - if err := r.createPod(ctx, browser, browserSpec); err != nil { + if err := r.createPod(ctx, browser, browserSpec, opts); err != nil { if errors.IsAlreadyExists(err) { log.Info("Browser Pod already exists, will reconcile on next iteration") return ctrl.Result{RequeueAfter: quickCheck}, nil @@ -375,10 +405,10 @@ func (r *BrowserReconciler) handleMissingPod(ctx context.Context, browser *brows } // createPod creates a Pod for Browser with optimized memory usage -func (r *BrowserReconciler) createPod(ctx context.Context, browser *browserv1.Browser, browserSpec *configv1.BrowserVersionConfigSpec) error { +func (r *BrowserReconciler) createPod(ctx context.Context, browser *browserv1.Browser, browserSpec *configv1.BrowserVersionConfigSpec, opts *SelenosisOptions) error { log := logger.FromContext(ctx) - pod := buildBrowserPod(browser, browserSpec) + pod := buildBrowserPod(browser, browserSpec, opts) log.Info("BrowserPodSpec configuration applied") return r.client.Create(ctx, pod) @@ -615,7 +645,7 @@ func (r *BrowserReconciler) deleteBrowser(ctx context.Context, browser *browserv return ctrl.Result{}, nil } -func buildBrowserPod(browser *browserv1.Browser, cfg *configv1.BrowserVersionConfigSpec) *corev1.Pod { +func buildBrowserPod(browser *browserv1.Browser, cfg *configv1.BrowserVersionConfigSpec, opts *SelenosisOptions) *corev1.Pod { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: browser.GetName(), @@ -778,6 +808,9 @@ func buildBrowserPod(browser *browserv1.Browser, cfg *configv1.BrowserVersionCon pod.Annotations = map[string]string{} } for k, v := range browser.Annotations { + if k == selenosisOptionsAnnotationKey { + continue + } pod.Annotations[k] = v } } @@ -818,6 +851,8 @@ func buildBrowserPod(browser *browserv1.Browser, cfg *configv1.BrowserVersionCon pod.Spec.Hostname = browser.GetName() pod.Spec.RestartPolicy = corev1.RestartPolicyNever + applySelenosisOptions(pod, opts) + return pod } @@ -827,3 +862,69 @@ func lenSidecars(cfg *configv1.BrowserVersionConfigSpec) int { } return 0 } + +func parseSelenosisOptions(ann map[string]string) (*SelenosisOptions, error) { + if ann == nil { + return nil, nil + } + raw := ann[selenosisOptionsAnnotationKey] + if raw == "" { + return nil, nil + } + + var doc SelenosisOptions + if err := json.Unmarshal([]byte(raw), &doc); err != nil { + return nil, fmt.Errorf("unmarshal %s: %w", selenosisOptionsAnnotationKey, err) + } + return &doc, nil +} + +func applySelenosisOptions(pod *corev1.Pod, opts *SelenosisOptions) { + if pod == nil || opts == nil { + return + } + + if len(opts.Containers) > 0 { + for i := range pod.Spec.Containers { + name := pod.Spec.Containers[i].Name + option, ok := opts.Containers[name] + if !ok || len(option.Env) == 0 { + continue + } + pod.Spec.Containers[i].Env = mergeEnvVars(pod.Spec.Containers[i].Env, option.Env) + } + } + + if opts.Labels != nil { + if pod.Labels == nil { + pod.Labels = map[string]string{} + } + for k, v := range opts.Labels { + pod.Labels[k] = v + } + } +} + +func mergeEnvVars(base []corev1.EnvVar, override map[string]string) []corev1.EnvVar { + if len(override) == 0 { + return base + } + + idx := make(map[string]int, len(base)) + out := append([]corev1.EnvVar(nil), base...) + for i := range out { + idx[out[i].Name] = i + } + + for k, v := range override { + ev := corev1.EnvVar{Name: k, Value: v} + if pos, ok := idx[k]; ok { + out[pos] = ev + } else { + idx[k] = len(out) + out = append(out, ev) + } + } + + return out +} diff --git a/controllers/browser/browser_reconciler_test.go b/controllers/browser/browser_reconciler_test.go index 283e717..bbc9822 100644 --- a/controllers/browser/browser_reconciler_test.go +++ b/controllers/browser/browser_reconciler_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "reflect" + "strings" "testing" "time" "unsafe" @@ -55,6 +56,15 @@ func setStoreConfig(t *testing.T, cfgStore *store.BrowserConfigStore, key string m[key] = spec } +func envValue(env []corev1.EnvVar, key string) (string, bool) { + for _, item := range env { + if item.Name == key { + return item.Value, true + } + } + return "", false +} + func TestContainerStateEqual(t *testing.T) { now := metav1.NewTime(time.Now().UTC()) a := corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: now}} @@ -149,7 +159,7 @@ func TestBuildBrowserPod(t *testing.T) { }, } - pod := buildBrowserPod(brw, cfg) + pod := buildBrowserPod(brw, cfg, nil) if pod.Name != "b1" || pod.Namespace != "ns" { t.Fatalf("unexpected pod identity") } @@ -167,6 +177,96 @@ func TestBuildBrowserPod(t *testing.T) { } } +func TestParseSelenosisOptionsInvalidJSON(t *testing.T) { + ann := map[string]string{ + selenosisOptionsAnnotationKey: "{nope", + } + _, err := parseSelenosisOptions(ann) + if err == nil { + t.Fatalf("expected error") + } + if !strings.Contains(err.Error(), selenosisOptionsAnnotationKey) { + t.Fatalf("expected error to mention annotation key, got %v", err) + } +} + +func TestParseSelenosisOptionsEmpty(t *testing.T) { + opts, err := parseSelenosisOptions(nil) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if opts != nil { + t.Fatalf("expected nil options for nil annotations") + } + + opts, err = parseSelenosisOptions(map[string]string{selenosisOptionsAnnotationKey: ""}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if opts != nil { + t.Fatalf("expected nil options for empty annotation") + } +} + +func TestParseSelenosisOptionsValidJSON(t *testing.T) { + ann := map[string]string{ + selenosisOptionsAnnotationKey: `{"labels":{"a":"b"},"containers":{"browser":{"env":{"X":"1"}}}}`, + } + opts, err := parseSelenosisOptions(ann) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if opts == nil || opts.Labels["a"] != "b" { + t.Fatalf("expected labels to be parsed") + } + if opts.Containers["browser"].Env["X"] != "1" { + t.Fatalf("expected container env to be parsed") + } +} + +func TestApplySelenosisOptionsMergesEnvAndLabels(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"existing": "1"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "browser", + Env: []corev1.EnvVar{ + {Name: "A", Value: "1"}, + {Name: "B", Value: "2"}, + }, + }, + {Name: "sidecar"}, + }, + }, + } + opts := &SelenosisOptions{ + Labels: map[string]string{"from": "options"}, + Containers: map[string]ContainerOption{ + "browser": {Env: map[string]string{"B": "override", "C": "new"}}, + }, + } + + applySelenosisOptions(pod, opts) + + if pod.Labels["existing"] != "1" || pod.Labels["from"] != "options" { + t.Fatalf("expected labels to be merged, got %+v", pod.Labels) + } + + env := pod.Spec.Containers[0].Env + if val, ok := envValue(env, "A"); !ok || val != "1" { + t.Fatalf("expected env A=1") + } + if val, ok := envValue(env, "B"); !ok || val != "override" { + t.Fatalf("expected env B override") + } + if val, ok := envValue(env, "C"); !ok || val != "new" { + t.Fatalf("expected env C new") + } +} + func TestHandleMissingPodConfigNotFound(t *testing.T) { scheme := newBrowserScheme(t) cfgStore := store.NewBrowserConfigStore() @@ -264,6 +364,61 @@ func TestHandleMissingPodCreatesPod(t *testing.T) { } } +func TestHandleMissingPodInvalidSelenosisOptions(t *testing.T) { + scheme := newBrowserScheme(t) + cfgStore := store.NewBrowserConfigStore() + spec := &configv1.BrowserVersionConfigSpec{Image: "img"} + setStoreConfig(t, cfgStore, "ns/chrome:120", spec) + + cl := newBrowserClient(scheme) + r := NewBrowserReconciler(cl, cfgStore, scheme) + + brw := &browserv1.Browser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b1", + Namespace: "ns", + Annotations: map[string]string{ + selenosisOptionsAnnotationKey: "{bad-json", + }, + }, + Spec: browserv1.BrowserSpec{ + BrowserName: "chrome", + BrowserVersion: "120", + }, + } + if err := cl.Create(context.Background(), brw); err != nil { + t.Fatalf("create browser: %v", err) + } + + res, err := r.handleMissingPod(context.Background(), brw) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if res.RequeueAfter != 0 { + t.Fatalf("expected no requeue, got %v", res.RequeueAfter) + } + + updated := &browserv1.Browser{} + if err := cl.Get(context.Background(), client.ObjectKey{Name: "b1", Namespace: "ns"}, updated); err != nil { + t.Fatalf("get browser: %v", err) + } + if updated.Status.Phase != corev1.PodFailed { + t.Fatalf("expected failed status, got %s", updated.Status.Phase) + } + if updated.Status.Reason != "InvalidSelenosisOptions" { + t.Fatalf("expected reason InvalidSelenosisOptions, got %s", updated.Status.Reason) + } + + pod := &corev1.Pod{} + err = cl.Get(context.Background(), client.ObjectKey{Name: "b1", Namespace: "ns"}, pod) + if err == nil { + t.Fatalf("expected no pod to be created") + } + if !apierrors.IsNotFound(err) { + t.Fatalf("expected not found error, got %v", err) + } +} + func TestUpdateBrowserStatusCriticalContainer(t *testing.T) { scheme := newBrowserScheme(t) cl := newBrowserClient(scheme) @@ -852,7 +1007,7 @@ func TestBuildBrowserPodWithInitContainersAndVolumes(t *testing.T) { }, } - pod := buildBrowserPod(brw, cfg) + pod := buildBrowserPod(brw, cfg, nil) if len(pod.Spec.InitContainers) != 1 { t.Fatalf("expected init container") } @@ -893,7 +1048,7 @@ func TestBuildBrowserPodInitContainerFields(t *testing.T) { }, } - pod := buildBrowserPod(brw, cfg) + pod := buildBrowserPod(brw, cfg, nil) if len(pod.Spec.InitContainers) != 1 { t.Fatalf("expected init container") } @@ -962,7 +1117,7 @@ func TestBuildBrowserPodAllFields(t *testing.T) { }, } - pod := buildBrowserPod(brw, cfg) + pod := buildBrowserPod(brw, cfg, nil) if pod.Spec.NodeSelector["k"] != "v" { t.Fatalf("expected node selector") } @@ -995,7 +1150,7 @@ func TestBuildBrowserPodBrowserLabelsOnly(t *testing.T) { }, } - pod := buildBrowserPod(brw, cfg) + pod := buildBrowserPod(brw, cfg, nil) if pod.Labels["only"] != "browser" { t.Fatalf("expected browser labels to be applied") } From fafa3588780750fd8eec182f86c59d1a98f21e50 Mon Sep 17 00:00:00 2001 From: alcounit Date: Tue, 20 Jan 2026 21:53:03 -0600 Subject: [PATCH 3/4] update var name --- controllers/browser/browser_reconciler.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/controllers/browser/browser_reconciler.go b/controllers/browser/browser_reconciler.go index f7de884..a0d4541 100644 --- a/controllers/browser/browser_reconciler.go +++ b/controllers/browser/browser_reconciler.go @@ -872,11 +872,11 @@ func parseSelenosisOptions(ann map[string]string) (*SelenosisOptions, error) { return nil, nil } - var doc SelenosisOptions - if err := json.Unmarshal([]byte(raw), &doc); err != nil { + var opts SelenosisOptions + if err := json.Unmarshal([]byte(raw), &opts); err != nil { return nil, fmt.Errorf("unmarshal %s: %w", selenosisOptionsAnnotationKey, err) } - return &doc, nil + return &opts, nil } func applySelenosisOptions(pod *corev1.Pod, opts *SelenosisOptions) { From 9fae49e3701e74ae1ad879a018b4e2d422aec1ca Mon Sep 17 00:00:00 2001 From: alcounit Date: Wed, 4 Mar 2026 17:08:26 -0600 Subject: [PATCH 4/4] Move BrowserConfig and Browser CRD's to separate group versions --- .gitignore | 3 + Makefile | 7 +- README.md | 4 + apis/browser/v1/doc.go | 2 +- apis/browser/v1/register.go | 4 +- apis/browserconfig/v1/browser_config.go | 5 +- apis/browserconfig/v1/doc.go | 2 +- apis/browserconfig/v1/register.go | 4 +- config/controller/browser-controller.yaml | 44 ---- ...aml => browser.selenosis.io_browsers.yaml} | 4 +- ...erconfig.selenosis.io_browserconfigs.yaml} | 4 +- .../examples/browser-config-multisidecar.yaml | 208 ------------------ .../browser-config-singlesidecar.yaml | 51 ----- config/rbac/role.yaml | 62 +++++- controllers/browser/browser_reconciler.go | 9 +- .../browserconfig/browserconfig_reconciler.go | 7 + pkg/clientset/clientset.go | 29 ++- pkg/clientset/fake/clientset_generated.go | 17 +- pkg/clientset/fake/register.go | 6 +- pkg/clientset/scheme/register.go | 6 +- pkg/clientset/typed/browser/v1/browser.go | 2 +- .../typed/browser/v1/browser_client.go | 30 +-- .../typed/browser/v1/fake/fake_browser.go | 4 +- .../browser/v1/fake/fake_browser_client.go | 6 +- .../typed/browserconfig/v1/browserconfig.go | 69 ++++++ .../browserconfig/v1/browserconfig_client.go | 100 +++++++++ pkg/clientset/typed/browserconfig/v1/doc.go | 19 ++ .../typed/browserconfig/v1/fake/doc.go | 19 ++ .../v1/fake/fake_browserconfig.go | 49 +++++ .../v1/fake/fake_browserconfig_client.go | 39 ++++ .../browserconfig/v1/generated_expansion.go | 20 ++ .../externalversions/browser/v1/browser.go | 8 +- .../browserconfig/interface.go | 45 ++++ .../browserconfig/v1/browserconfig.go | 101 +++++++++ .../browserconfig/v1/interface.go | 44 ++++ pkg/informers/externalversions/factory.go | 10 +- pkg/informers/externalversions/generic.go | 9 +- pkg/listers/browserconfig/v1/browserconfig.go | 69 ++++++ .../browserconfig/v1/expansion_generated.go | 26 +++ store/browserconfig_store.go | 40 +++- store/browserconfig_store_test.go | 44 +++- 41 files changed, 843 insertions(+), 388 deletions(-) delete mode 100644 config/controller/browser-controller.yaml rename config/crd/{selenosis.io_browsers.yaml => browser.selenosis.io_browsers.yaml} (99%) rename config/crd/{selenosis.io_browserconfigs.yaml => browserconfig.selenosis.io_browserconfigs.yaml} (99%) delete mode 100644 config/examples/browser-config-multisidecar.yaml delete mode 100644 config/examples/browser-config-singlesidecar.yaml create mode 100644 pkg/clientset/typed/browserconfig/v1/browserconfig.go create mode 100644 pkg/clientset/typed/browserconfig/v1/browserconfig_client.go create mode 100644 pkg/clientset/typed/browserconfig/v1/doc.go create mode 100644 pkg/clientset/typed/browserconfig/v1/fake/doc.go create mode 100644 pkg/clientset/typed/browserconfig/v1/fake/fake_browserconfig.go create mode 100644 pkg/clientset/typed/browserconfig/v1/fake/fake_browserconfig_client.go create mode 100644 pkg/clientset/typed/browserconfig/v1/generated_expansion.go create mode 100644 pkg/informers/externalversions/browserconfig/interface.go create mode 100644 pkg/informers/externalversions/browserconfig/v1/browserconfig.go create mode 100644 pkg/informers/externalversions/browserconfig/v1/interface.go create mode 100644 pkg/listers/browserconfig/v1/browserconfig.go create mode 100644 pkg/listers/browserconfig/v1/expansion_generated.go diff --git a/.gitignore b/.gitignore index e43b0f9..7229216 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .DS_Store +Dockerfile.local +Makefile.local +out/ \ No newline at end of file diff --git a/Makefile b/Makefile index 125f5e8..387a234 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ MODULE := github.com/alcounit/browser-controller APIS_PKG := apis GROUP := selenosis BROWSER := browser +BROWSERCONFIG := browserconfig API_VERSION := v1 BOILERPLATE := hack/boilerplate.go.txt @@ -57,7 +58,7 @@ client: @$(CLIENT_GEN) \ --clientset-name clientset \ --input-base "$(MODULE)/apis" \ - --input $(BROWSER)/$(API_VERSION) \ + --input $(BROWSER)/$(API_VERSION),$(BROWSERCONFIG)/$(API_VERSION) \ --output-pkg $(PROJECT_PKG) \ --output-dir ./pkg \ --go-header-file $(BOILERPLATE) @@ -67,7 +68,7 @@ lister: --output-pkg $(LISTER_PKG) \ --output-dir ./pkg/listers \ --go-header-file $(BOILERPLATE) \ - $(MODULE)/$(APIS_PKG)/$(BROWSER)/$(API_VERSION) + $(MODULE)/$(APIS_PKG)/$(BROWSER)/$(API_VERSION) $(MODULE)/$(APIS_PKG)/$(BROWSERCONFIG)/$(API_VERSION) informer: @$(INFORMER_GEN) \ @@ -76,7 +77,7 @@ informer: --output-pkg $(INFORMER_PKG) \ --output-dir ./pkg/informers \ --go-header-file $(BOILERPLATE) \ - $(MODULE)/$(APIS_PKG)/$(BROWSER)/$(API_VERSION) + $(MODULE)/$(APIS_PKG)/$(BROWSER)/$(API_VERSION) $(MODULE)/$(APIS_PKG)/$(BROWSERCONFIG)/$(API_VERSION) manifests: @$(CONTROLLER_GEN) \ diff --git a/README.md b/README.md index 441a90d..bcc9460 100644 --- a/README.md +++ b/README.md @@ -350,3 +350,7 @@ Or combined: ```bash make deploy ``` + +## Deployment + +Helm chart [selenosis-deploy](https://github.com/alcounit/selenosis-deploy) \ No newline at end of file diff --git a/apis/browser/v1/doc.go b/apis/browser/v1/doc.go index 8548e67..b1f4e13 100644 --- a/apis/browser/v1/doc.go +++ b/apis/browser/v1/doc.go @@ -1,4 +1,4 @@ // +k8s:deepcopy-gen=package -// +groupName=selenosis.io +// +groupName=browser.selenosis.io package v1 diff --git a/apis/browser/v1/register.go b/apis/browser/v1/register.go index 9d77f15..c47a577 100644 --- a/apis/browser/v1/register.go +++ b/apis/browser/v1/register.go @@ -16,7 +16,7 @@ limitations under the License. // Package v1 contains API Schema definitions for the selenosis.io v1 API group. // +kubebuilder:object:generate=true -// +groupName=selenosis.io +// +groupName=browser.selenosis.io package v1 import ( @@ -27,7 +27,7 @@ import ( var ( // GroupVersion is group version used to register these objects. - SchemeGroupVersion = schema.GroupVersion{Group: "selenosis.io", Version: "v1"} + SchemeGroupVersion = schema.GroupVersion{Group: "browser.selenosis.io", Version: "v1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) diff --git a/apis/browserconfig/v1/browser_config.go b/apis/browserconfig/v1/browser_config.go index 1d12ea0..063ea6f 100644 --- a/apis/browserconfig/v1/browser_config.go +++ b/apis/browserconfig/v1/browser_config.go @@ -5,9 +5,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// BrowserConfig is the root CRD type that defines browser configurations and associated pod templates. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:resource:path=browserconfigs,scope=Namespaced +// BrowserConfig is the root CRD type that defines browser configurations and associated pod templates. type BrowserConfig struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/apis/browserconfig/v1/doc.go b/apis/browserconfig/v1/doc.go index 8548e67..d3570bb 100644 --- a/apis/browserconfig/v1/doc.go +++ b/apis/browserconfig/v1/doc.go @@ -1,4 +1,4 @@ // +k8s:deepcopy-gen=package -// +groupName=selenosis.io +// +groupName=browserconfig.selenosis.io package v1 diff --git a/apis/browserconfig/v1/register.go b/apis/browserconfig/v1/register.go index 379a119..7128eae 100644 --- a/apis/browserconfig/v1/register.go +++ b/apis/browserconfig/v1/register.go @@ -16,7 +16,7 @@ limitations under the License. // Package v1 contains API Schema definitions for the selenosis.io v1 API group. // +kubebuilder:object:generate=true -// +groupName=selenosis.io +// +groupName=browserconfig.selenosis.io package v1 import ( @@ -27,7 +27,7 @@ import ( var ( // GroupVersion is group version used to register these objects. - SchemeGroupVersion = schema.GroupVersion{Group: "selenosis.io", Version: "v1"} + SchemeGroupVersion = schema.GroupVersion{Group: "browserconfig.selenosis.io", Version: "v1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) diff --git a/config/controller/browser-controller.yaml b/config/controller/browser-controller.yaml deleted file mode 100644 index a88b91e..0000000 --- a/config/controller/browser-controller.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: browser-controller - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - role: browser-controller - template: - metadata: - labels: - role: browser-controller - spec: - serviceAccountName: browser-controller - containers: - - name: manager - image: alcounit/browser-controller:latest - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8080 - name: http - resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 100m - memory: 64Mi - livenessProbe: - httpGet: - path: /healthz - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - - diff --git a/config/crd/selenosis.io_browsers.yaml b/config/crd/browser.selenosis.io_browsers.yaml similarity index 99% rename from config/crd/selenosis.io_browsers.yaml rename to config/crd/browser.selenosis.io_browsers.yaml index 0d05270..7604dce 100644 --- a/config/crd/selenosis.io_browsers.yaml +++ b/config/crd/browser.selenosis.io_browsers.yaml @@ -4,9 +4,9 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 - name: browsers.selenosis.io + name: browsers.browser.selenosis.io spec: - group: selenosis.io + group: browser.selenosis.io names: kind: Browser listKind: BrowserList diff --git a/config/crd/selenosis.io_browserconfigs.yaml b/config/crd/browserconfig.selenosis.io_browserconfigs.yaml similarity index 99% rename from config/crd/selenosis.io_browserconfigs.yaml rename to config/crd/browserconfig.selenosis.io_browserconfigs.yaml index bcf56d5..02ed431 100644 --- a/config/crd/selenosis.io_browserconfigs.yaml +++ b/config/crd/browserconfig.selenosis.io_browserconfigs.yaml @@ -4,9 +4,9 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 - name: browserconfigs.selenosis.io + name: browserconfigs.browserconfig.selenosis.io spec: - group: selenosis.io + group: browserconfig.selenosis.io names: kind: BrowserConfig listKind: BrowserConfigList diff --git a/config/examples/browser-config-multisidecar.yaml b/config/examples/browser-config-multisidecar.yaml deleted file mode 100644 index b6521a0..0000000 --- a/config/examples/browser-config-multisidecar.yaml +++ /dev/null @@ -1,208 +0,0 @@ -apiVersion: selenosis.io/v1 -kind: BrowserConfig -metadata: - name: chrome-config-multisidecar -spec: - template: - labels: - app: browser - env: - - name: SCREEN_RESOLUTION - value: 1920x1080 - - name: DISPLAY - value: "127.0.0.1:0" - - workingDir: /home/user - - securityContext: - runAsGroup: 4096 - runAsUser: 4096 - - resources: - requests: - cpu: "250m" - memory: "512Mi" - limits: - cpu: "500m" - memory: "1Gi" - - sidecars: - - name: seleniferous - image: "alcounit/seleniferous:latest" - env: - - name: SESSION_CREATE_TIMEOUT - value: "10m" - - name: SESSION_IDLE_TIMEOUT - value: "10m" - - name: BROWSER_PATH - value: "/" - - name: BROWSER_PORT - value: "4444" - - name: DISPLAY - value: "127.0.0.1:0" - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - resources: - limits: - cpu: "0.2" - memory: "128Mi" - requests: - cpu: "0.1" - memory: "64Mi" - volumeMounts: - - mountPath: /dev/shm - name: dshm - - mountPath: /tmp - name: tmp - - mountPath: /etc/passwd - name: usergroup - subPath: passwd - - mountPath: /etc/group - name: usergroup - subPath: group - - mountPath: /home/user - name: home - - mountPath: /home/user/Downloads - name: downloads - - name: x-server - env: - - name: SCREEN_RESOLUTION - value: 1920x1080 - - name: DISPLAY - value: "127.0.0.1:0" - image: quay.io/aerokube/xvfb:21.1-1 - ports: - - containerPort: 6000 - resources: - limits: - cpu: 200m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - volumeMounts: - - mountPath: /dev/shm - name: dshm - - mountPath: /tmp - name: tmp - - mountPath: /etc/group - name: usergroup - subPath: group - - mountPath: /etc/passwd - name: usergroup - subPath: passwd - - mountPath: /home/user - name: home - - mountPath: /home/user/Downloads - name: downloads - workingDir: /home/user - - name: window-manager - env: - - name: DISPLAY - value: "127.0.0.1:0" - image: quay.io/aerokube/openbox:3.6.1-1 - resources: - limits: - cpu: 200m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - volumeMounts: - - mountPath: /dev/shm - name: dshm - - mountPath: /tmp - name: tmp - - mountPath: /etc/group - name: usergroup - subPath: group - - mountPath: /etc/passwd - name: usergroup - subPath: passwd - - mountPath: /home/user - name: home - - mountPath: /home/user/Downloads - name: downloads - workingDir: /home/user - - name: vnc-server - env: - - name: DISPLAY - value: "127.0.0.1:0" - image: quay.io/aerokube/x11vnc:0.9.16-1 - ports: - - containerPort: 5900 - resources: - limits: - cpu: 200m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - volumeMounts: - - mountPath: /dev/shm - name: dshm - - mountPath: /tmp - name: tmp - - mountPath: /etc/group - name: usergroup - subPath: group - - mountPath: /etc/passwd - name: usergroup - subPath: passwd - - mountPath: /home/user - name: home - - mountPath: /home/user/Downloads - name: downloads - workingDir: /home/user - - volumes: - - emptyDir: - medium: Memory - name: dshm - - emptyDir: - medium: Memory - name: tmp - - emptyDir: - medium: Memory - name: home - - emptyDir: {} - name: downloads - - configMap: - defaultMode: 420 - name: usergroup - name: usergroup - - volumeMounts: - - mountPath: /dev/shm - name: dshm - - mountPath: /tmp - name: tmp - - mountPath: /etc/group - name: usergroup - subPath: group - - mountPath: /etc/passwd - name: usergroup - subPath: passwd - - mountPath: /home/user - name: home - - mountPath: /home/user/Downloads - name: downloads - - browsers: - chrome: - "139.0": - image: quay.io/browser/google-chrome-stable:139.0 ---- -apiVersion: v1 -data: - group: | - root:x:0: - user:x:4096: - passwd: | - root:x:0:0:root:/root:/bin/bash - user:x:4096:4096::/home/user:/usr/sbin/nologin -kind: ConfigMap -metadata: - name: usergroup \ No newline at end of file diff --git a/config/examples/browser-config-singlesidecar.yaml b/config/examples/browser-config-singlesidecar.yaml deleted file mode 100644 index dd49c56..0000000 --- a/config/examples/browser-config-singlesidecar.yaml +++ /dev/null @@ -1,51 +0,0 @@ -apiVersion: selenosis.io/v1 -kind: BrowserConfig -metadata: - name: chrome-config-singlesidecar -spec: - template: - labels: - app: browser - env: - - name: SE_VNC_PASSWORD - value: selenoid - - resources: - requests: - cpu: "250m" - memory: "512Mi" - limits: - cpu: "500m" - memory: "1Gi" - - sidecars: - - name: seleniferous - image: "192.168.1.101:30000/seleniferous:latest" - imagePullPolicy: IfNotPresent - env: - - name: SESSION_CREATE_TIMEOUT - value: "10m" - - name: SESSION_IDLE_TIMEOUT - value: "10m" - - name: BROWSER_PATH - value: "/" - - name: BROWSER_PORT - value: "4444" - - name: DISPLAY - value: "127.0.0.1:0" - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - resources: - limits: - cpu: "0.2" - memory: "128Mi" - requests: - cpu: "0.1" - memory: "64Mi" - - browsers: - chrome: - "143.0": - image: 192.168.1.101:30000/standalone-chrome:143.0-20251212 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 599c496..aa9e50e 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -5,7 +5,26 @@ metadata: name: browser-controller rules: - apiGroups: - - selenosis.io + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - browser.selenosis.io resources: - browsers verbs: @@ -17,16 +36,53 @@ rules: - update - watch - apiGroups: - - selenosis.io + - browser.selenosis.io resources: - browsers/finalizers verbs: - update - apiGroups: - - selenosis.io + - browser.selenosis.io resources: - browsers/status verbs: - get - patch - update +- apiGroups: + - browserconfig.selenosis.io + resources: + - browserconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - browserconfig.selenosis.io + resources: + - browserconfigs/finalizers + verbs: + - update +- apiGroups: + - browserconfig.selenosis.io + resources: + - browserconfigs/status + verbs: + - get + - patch + - update +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - list + - patch + - update + - watch diff --git a/controllers/browser/browser_reconciler.go b/controllers/browser/browser_reconciler.go index 65ce483..e4ce445 100644 --- a/controllers/browser/browser_reconciler.go +++ b/controllers/browser/browser_reconciler.go @@ -44,9 +44,12 @@ type ContainerOption struct { Env map[string]string `json:"env,omitempty"` } -// +kubebuilder:rbac:groups=selenosis.io,resources=browsers,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=selenosis.io,resources=browsers/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=selenosis.io,resources=browsers/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups=browser.selenosis.io,resources=browsers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=browser.selenosis.io,resources=browsers/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=browser.selenosis.io,resources=browsers/finalizers,verbs=update // BrowserReconciler reconciles Browser resources type BrowserReconciler struct { diff --git a/controllers/browserconfig/browserconfig_reconciler.go b/controllers/browserconfig/browserconfig_reconciler.go index 5885e2a..04eff08 100644 --- a/controllers/browserconfig/browserconfig_reconciler.go +++ b/controllers/browserconfig/browserconfig_reconciler.go @@ -20,6 +20,11 @@ const ( mediumRetry = time.Second * 10 ) +// +kubebuilder:rbac:groups=browserconfig.selenosis.io,resources=browserconfigs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=browserconfig.selenosis.io,resources=browserconfigs/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=browserconfig.selenosis.io,resources=browserconfigs/finalizers,verbs=update + +// BrowserConfigReconciler reconciles BrowserConfig resources type BrowserConfigReconciler struct { client client.Client scheme *runtime.Scheme @@ -32,12 +37,14 @@ func NewBrowserConfigReconciler(client client.Client, scheme *runtime.Scheme) *B } } +// SetupWithManager sets up the controller with the Manager func (r *BrowserConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&configv1.BrowserConfig{}). Complete(r) } +// Reconcile synchronizes the state of BrowserConfig func (r BrowserConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logger.FromContext(ctx) diff --git a/pkg/clientset/clientset.go b/pkg/clientset/clientset.go index 573b677..e8d40b8 100644 --- a/pkg/clientset/clientset.go +++ b/pkg/clientset/clientset.go @@ -21,7 +21,8 @@ import ( fmt "fmt" http "net/http" - selenosisv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browser/v1" + browserv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browser/v1" + browserconfigv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browserconfig/v1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" @@ -29,18 +30,25 @@ import ( type Interface interface { Discovery() discovery.DiscoveryInterface - SelenosisV1() selenosisv1.SelenosisV1Interface + BrowserV1() browserv1.BrowserV1Interface + BrowserconfigV1() browserconfigv1.BrowserconfigV1Interface } // Clientset contains the clients for groups. type Clientset struct { *discovery.DiscoveryClient - selenosisV1 *selenosisv1.SelenosisV1Client + browserV1 *browserv1.BrowserV1Client + browserconfigV1 *browserconfigv1.BrowserconfigV1Client } -// SelenosisV1 retrieves the SelenosisV1Client -func (c *Clientset) SelenosisV1() selenosisv1.SelenosisV1Interface { - return c.selenosisV1 +// BrowserV1 retrieves the BrowserV1Client +func (c *Clientset) BrowserV1() browserv1.BrowserV1Interface { + return c.browserV1 +} + +// BrowserconfigV1 retrieves the BrowserconfigV1Client +func (c *Clientset) BrowserconfigV1() browserconfigv1.BrowserconfigV1Interface { + return c.browserconfigV1 } // Discovery retrieves the DiscoveryClient @@ -87,7 +95,11 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, var cs Clientset var err error - cs.selenosisV1, err = selenosisv1.NewForConfigAndClient(&configShallowCopy, httpClient) + cs.browserV1, err = browserv1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + cs.browserconfigV1, err = browserconfigv1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } @@ -112,7 +124,8 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { // New creates a new Clientset for the given RESTClient. func New(c rest.Interface) *Clientset { var cs Clientset - cs.selenosisV1 = selenosisv1.New(c) + cs.browserV1 = browserv1.New(c) + cs.browserconfigV1 = browserconfigv1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs diff --git a/pkg/clientset/fake/clientset_generated.go b/pkg/clientset/fake/clientset_generated.go index 77e35cd..003d104 100644 --- a/pkg/clientset/fake/clientset_generated.go +++ b/pkg/clientset/fake/clientset_generated.go @@ -19,8 +19,10 @@ package fake import ( clientset "github.com/alcounit/browser-controller/pkg/clientset" - selenosisv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browser/v1" - fakeselenosisv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browser/v1/fake" + browserv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browser/v1" + fakebrowserv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browser/v1/fake" + browserconfigv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browserconfig/v1" + fakebrowserconfigv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browserconfig/v1/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" @@ -87,7 +89,12 @@ var ( _ testing.FakeClient = &Clientset{} ) -// SelenosisV1 retrieves the SelenosisV1Client -func (c *Clientset) SelenosisV1() selenosisv1.SelenosisV1Interface { - return &fakeselenosisv1.FakeSelenosisV1{Fake: &c.Fake} +// BrowserV1 retrieves the BrowserV1Client +func (c *Clientset) BrowserV1() browserv1.BrowserV1Interface { + return &fakebrowserv1.FakeBrowserV1{Fake: &c.Fake} +} + +// BrowserconfigV1 retrieves the BrowserconfigV1Client +func (c *Clientset) BrowserconfigV1() browserconfigv1.BrowserconfigV1Interface { + return &fakebrowserconfigv1.FakeBrowserconfigV1{Fake: &c.Fake} } diff --git a/pkg/clientset/fake/register.go b/pkg/clientset/fake/register.go index 932c794..8124b42 100644 --- a/pkg/clientset/fake/register.go +++ b/pkg/clientset/fake/register.go @@ -18,7 +18,8 @@ limitations under the License. package fake import ( - selenosisv1 "github.com/alcounit/browser-controller/apis/browser/v1" + browserv1 "github.com/alcounit/browser-controller/apis/browser/v1" + browserconfigv1 "github.com/alcounit/browser-controller/apis/browserconfig/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -30,7 +31,8 @@ var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ - selenosisv1.AddToScheme, + browserv1.AddToScheme, + browserconfigv1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/clientset/scheme/register.go b/pkg/clientset/scheme/register.go index b76ce9c..4741435 100644 --- a/pkg/clientset/scheme/register.go +++ b/pkg/clientset/scheme/register.go @@ -18,7 +18,8 @@ limitations under the License. package scheme import ( - selenosisv1 "github.com/alcounit/browser-controller/apis/browser/v1" + browserv1 "github.com/alcounit/browser-controller/apis/browser/v1" + browserconfigv1 "github.com/alcounit/browser-controller/apis/browserconfig/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -30,7 +31,8 @@ var Scheme = runtime.NewScheme() var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ - selenosisv1.AddToScheme, + browserv1.AddToScheme, + browserconfigv1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/clientset/typed/browser/v1/browser.go b/pkg/clientset/typed/browser/v1/browser.go index 73163d0..29cdb60 100644 --- a/pkg/clientset/typed/browser/v1/browser.go +++ b/pkg/clientset/typed/browser/v1/browser.go @@ -55,7 +55,7 @@ type browsers struct { } // newBrowsers returns a Browsers -func newBrowsers(c *SelenosisV1Client, namespace string) *browsers { +func newBrowsers(c *BrowserV1Client, namespace string) *browsers { return &browsers{ gentype.NewClientWithList[*browserv1.Browser, *browserv1.BrowserList]( "browsers", diff --git a/pkg/clientset/typed/browser/v1/browser_client.go b/pkg/clientset/typed/browser/v1/browser_client.go index 03dfa83..b7506ca 100644 --- a/pkg/clientset/typed/browser/v1/browser_client.go +++ b/pkg/clientset/typed/browser/v1/browser_client.go @@ -25,24 +25,24 @@ import ( rest "k8s.io/client-go/rest" ) -type SelenosisV1Interface interface { +type BrowserV1Interface interface { RESTClient() rest.Interface BrowsersGetter } -// SelenosisV1Client is used to interact with features provided by the selenosis.io group. -type SelenosisV1Client struct { +// BrowserV1Client is used to interact with features provided by the browser.selenosis.io group. +type BrowserV1Client struct { restClient rest.Interface } -func (c *SelenosisV1Client) Browsers(namespace string) BrowserInterface { +func (c *BrowserV1Client) Browsers(namespace string) BrowserInterface { return newBrowsers(c, namespace) } -// NewForConfig creates a new SelenosisV1Client for the given config. +// NewForConfig creates a new BrowserV1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). -func NewForConfig(c *rest.Config) (*SelenosisV1Client, error) { +func NewForConfig(c *rest.Config) (*BrowserV1Client, error) { config := *c setConfigDefaults(&config) httpClient, err := rest.HTTPClientFor(&config) @@ -52,21 +52,21 @@ func NewForConfig(c *rest.Config) (*SelenosisV1Client, error) { return NewForConfigAndClient(&config, httpClient) } -// NewForConfigAndClient creates a new SelenosisV1Client for the given config and http client. +// NewForConfigAndClient creates a new BrowserV1Client for the given config and http client. // Note the http client provided takes precedence over the configured transport values. -func NewForConfigAndClient(c *rest.Config, h *http.Client) (*SelenosisV1Client, error) { +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*BrowserV1Client, error) { config := *c setConfigDefaults(&config) client, err := rest.RESTClientForConfigAndClient(&config, h) if err != nil { return nil, err } - return &SelenosisV1Client{client}, nil + return &BrowserV1Client{client}, nil } -// NewForConfigOrDie creates a new SelenosisV1Client for the given config and +// NewForConfigOrDie creates a new BrowserV1Client for the given config and // panics if there is an error in the config. -func NewForConfigOrDie(c *rest.Config) *SelenosisV1Client { +func NewForConfigOrDie(c *rest.Config) *BrowserV1Client { client, err := NewForConfig(c) if err != nil { panic(err) @@ -74,9 +74,9 @@ func NewForConfigOrDie(c *rest.Config) *SelenosisV1Client { return client } -// New creates a new SelenosisV1Client for the given RESTClient. -func New(c rest.Interface) *SelenosisV1Client { - return &SelenosisV1Client{c} +// New creates a new BrowserV1Client for the given RESTClient. +func New(c rest.Interface) *BrowserV1Client { + return &BrowserV1Client{c} } func setConfigDefaults(config *rest.Config) { @@ -92,7 +92,7 @@ func setConfigDefaults(config *rest.Config) { // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. -func (c *SelenosisV1Client) RESTClient() rest.Interface { +func (c *BrowserV1Client) RESTClient() rest.Interface { if c == nil { return nil } diff --git a/pkg/clientset/typed/browser/v1/fake/fake_browser.go b/pkg/clientset/typed/browser/v1/fake/fake_browser.go index 57c1e2a..05ed6a6 100644 --- a/pkg/clientset/typed/browser/v1/fake/fake_browser.go +++ b/pkg/clientset/typed/browser/v1/fake/fake_browser.go @@ -26,10 +26,10 @@ import ( // fakeBrowsers implements BrowserInterface type fakeBrowsers struct { *gentype.FakeClientWithList[*v1.Browser, *v1.BrowserList] - Fake *FakeSelenosisV1 + Fake *FakeBrowserV1 } -func newFakeBrowsers(fake *FakeSelenosisV1, namespace string) browserv1.BrowserInterface { +func newFakeBrowsers(fake *FakeBrowserV1, namespace string) browserv1.BrowserInterface { return &fakeBrowsers{ gentype.NewFakeClientWithList[*v1.Browser, *v1.BrowserList]( fake.Fake, diff --git a/pkg/clientset/typed/browser/v1/fake/fake_browser_client.go b/pkg/clientset/typed/browser/v1/fake/fake_browser_client.go index e4f5997..cb7757f 100644 --- a/pkg/clientset/typed/browser/v1/fake/fake_browser_client.go +++ b/pkg/clientset/typed/browser/v1/fake/fake_browser_client.go @@ -23,17 +23,17 @@ import ( testing "k8s.io/client-go/testing" ) -type FakeSelenosisV1 struct { +type FakeBrowserV1 struct { *testing.Fake } -func (c *FakeSelenosisV1) Browsers(namespace string) v1.BrowserInterface { +func (c *FakeBrowserV1) Browsers(namespace string) v1.BrowserInterface { return newFakeBrowsers(c, namespace) } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. -func (c *FakeSelenosisV1) RESTClient() rest.Interface { +func (c *FakeBrowserV1) RESTClient() rest.Interface { var ret *rest.RESTClient return ret } diff --git a/pkg/clientset/typed/browserconfig/v1/browserconfig.go b/pkg/clientset/typed/browserconfig/v1/browserconfig.go new file mode 100644 index 0000000..69f42e1 --- /dev/null +++ b/pkg/clientset/typed/browserconfig/v1/browserconfig.go @@ -0,0 +1,69 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1 + +import ( + context "context" + + browserconfigv1 "github.com/alcounit/browser-controller/apis/browserconfig/v1" + scheme "github.com/alcounit/browser-controller/pkg/clientset/scheme" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// BrowserConfigsGetter has a method to return a BrowserConfigInterface. +// A group's client should implement this interface. +type BrowserConfigsGetter interface { + BrowserConfigs(namespace string) BrowserConfigInterface +} + +// BrowserConfigInterface has methods to work with BrowserConfig resources. +type BrowserConfigInterface interface { + Create(ctx context.Context, browserConfig *browserconfigv1.BrowserConfig, opts metav1.CreateOptions) (*browserconfigv1.BrowserConfig, error) + Update(ctx context.Context, browserConfig *browserconfigv1.BrowserConfig, opts metav1.UpdateOptions) (*browserconfigv1.BrowserConfig, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, browserConfig *browserconfigv1.BrowserConfig, opts metav1.UpdateOptions) (*browserconfigv1.BrowserConfig, error) + Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error + Get(ctx context.Context, name string, opts metav1.GetOptions) (*browserconfigv1.BrowserConfig, error) + List(ctx context.Context, opts metav1.ListOptions) (*browserconfigv1.BrowserConfigList, error) + Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *browserconfigv1.BrowserConfig, err error) + BrowserConfigExpansion +} + +// browserConfigs implements BrowserConfigInterface +type browserConfigs struct { + *gentype.ClientWithList[*browserconfigv1.BrowserConfig, *browserconfigv1.BrowserConfigList] +} + +// newBrowserConfigs returns a BrowserConfigs +func newBrowserConfigs(c *BrowserconfigV1Client, namespace string) *browserConfigs { + return &browserConfigs{ + gentype.NewClientWithList[*browserconfigv1.BrowserConfig, *browserconfigv1.BrowserConfigList]( + "browserconfigs", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *browserconfigv1.BrowserConfig { return &browserconfigv1.BrowserConfig{} }, + func() *browserconfigv1.BrowserConfigList { return &browserconfigv1.BrowserConfigList{} }, + ), + } +} diff --git a/pkg/clientset/typed/browserconfig/v1/browserconfig_client.go b/pkg/clientset/typed/browserconfig/v1/browserconfig_client.go new file mode 100644 index 0000000..26f5219 --- /dev/null +++ b/pkg/clientset/typed/browserconfig/v1/browserconfig_client.go @@ -0,0 +1,100 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1 + +import ( + http "net/http" + + browserconfigv1 "github.com/alcounit/browser-controller/apis/browserconfig/v1" + scheme "github.com/alcounit/browser-controller/pkg/clientset/scheme" + rest "k8s.io/client-go/rest" +) + +type BrowserconfigV1Interface interface { + RESTClient() rest.Interface + BrowserConfigsGetter +} + +// BrowserconfigV1Client is used to interact with features provided by the browserconfig.selenosis.io group. +type BrowserconfigV1Client struct { + restClient rest.Interface +} + +func (c *BrowserconfigV1Client) BrowserConfigs(namespace string) BrowserConfigInterface { + return newBrowserConfigs(c, namespace) +} + +// NewForConfig creates a new BrowserconfigV1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*BrowserconfigV1Client, error) { + config := *c + setConfigDefaults(&config) + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new BrowserconfigV1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*BrowserconfigV1Client, error) { + config := *c + setConfigDefaults(&config) + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &BrowserconfigV1Client{client}, nil +} + +// NewForConfigOrDie creates a new BrowserconfigV1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *BrowserconfigV1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new BrowserconfigV1Client for the given RESTClient. +func New(c rest.Interface) *BrowserconfigV1Client { + return &BrowserconfigV1Client{c} +} + +func setConfigDefaults(config *rest.Config) { + gv := browserconfigv1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *BrowserconfigV1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/clientset/typed/browserconfig/v1/doc.go b/pkg/clientset/typed/browserconfig/v1/doc.go new file mode 100644 index 0000000..ea15b9b --- /dev/null +++ b/pkg/clientset/typed/browserconfig/v1/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1 diff --git a/pkg/clientset/typed/browserconfig/v1/fake/doc.go b/pkg/clientset/typed/browserconfig/v1/fake/doc.go new file mode 100644 index 0000000..9829597 --- /dev/null +++ b/pkg/clientset/typed/browserconfig/v1/fake/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/clientset/typed/browserconfig/v1/fake/fake_browserconfig.go b/pkg/clientset/typed/browserconfig/v1/fake/fake_browserconfig.go new file mode 100644 index 0000000..b1fd5b3 --- /dev/null +++ b/pkg/clientset/typed/browserconfig/v1/fake/fake_browserconfig.go @@ -0,0 +1,49 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1 "github.com/alcounit/browser-controller/apis/browserconfig/v1" + browserconfigv1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browserconfig/v1" + gentype "k8s.io/client-go/gentype" +) + +// fakeBrowserConfigs implements BrowserConfigInterface +type fakeBrowserConfigs struct { + *gentype.FakeClientWithList[*v1.BrowserConfig, *v1.BrowserConfigList] + Fake *FakeBrowserconfigV1 +} + +func newFakeBrowserConfigs(fake *FakeBrowserconfigV1, namespace string) browserconfigv1.BrowserConfigInterface { + return &fakeBrowserConfigs{ + gentype.NewFakeClientWithList[*v1.BrowserConfig, *v1.BrowserConfigList]( + fake.Fake, + namespace, + v1.SchemeGroupVersion.WithResource("browserconfigs"), + v1.SchemeGroupVersion.WithKind("BrowserConfig"), + func() *v1.BrowserConfig { return &v1.BrowserConfig{} }, + func() *v1.BrowserConfigList { return &v1.BrowserConfigList{} }, + func(dst, src *v1.BrowserConfigList) { dst.ListMeta = src.ListMeta }, + func(list *v1.BrowserConfigList) []*v1.BrowserConfig { return gentype.ToPointerSlice(list.Items) }, + func(list *v1.BrowserConfigList, items []*v1.BrowserConfig) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/clientset/typed/browserconfig/v1/fake/fake_browserconfig_client.go b/pkg/clientset/typed/browserconfig/v1/fake/fake_browserconfig_client.go new file mode 100644 index 0000000..08ef025 --- /dev/null +++ b/pkg/clientset/typed/browserconfig/v1/fake/fake_browserconfig_client.go @@ -0,0 +1,39 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1 "github.com/alcounit/browser-controller/pkg/clientset/typed/browserconfig/v1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeBrowserconfigV1 struct { + *testing.Fake +} + +func (c *FakeBrowserconfigV1) BrowserConfigs(namespace string) v1.BrowserConfigInterface { + return newFakeBrowserConfigs(c, namespace) +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeBrowserconfigV1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/clientset/typed/browserconfig/v1/generated_expansion.go b/pkg/clientset/typed/browserconfig/v1/generated_expansion.go new file mode 100644 index 0000000..781549c --- /dev/null +++ b/pkg/clientset/typed/browserconfig/v1/generated_expansion.go @@ -0,0 +1,20 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1 + +type BrowserConfigExpansion interface{} diff --git a/pkg/informers/externalversions/browser/v1/browser.go b/pkg/informers/externalversions/browser/v1/browser.go index 862c9e9..aec0123 100644 --- a/pkg/informers/externalversions/browser/v1/browser.go +++ b/pkg/informers/externalversions/browser/v1/browser.go @@ -61,25 +61,25 @@ func NewFilteredBrowserInformer(client clientset.Interface, namespace string, re if tweakListOptions != nil { tweakListOptions(&options) } - return client.SelenosisV1().Browsers(namespace).List(context.Background(), options) + return client.BrowserV1().Browsers(namespace).List(context.Background(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SelenosisV1().Browsers(namespace).Watch(context.Background(), options) + return client.BrowserV1().Browsers(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SelenosisV1().Browsers(namespace).List(ctx, options) + return client.BrowserV1().Browsers(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SelenosisV1().Browsers(namespace).Watch(ctx, options) + return client.BrowserV1().Browsers(namespace).Watch(ctx, options) }, }, &apisbrowserv1.Browser{}, diff --git a/pkg/informers/externalversions/browserconfig/interface.go b/pkg/informers/externalversions/browserconfig/interface.go new file mode 100644 index 0000000..4a33c18 --- /dev/null +++ b/pkg/informers/externalversions/browserconfig/interface.go @@ -0,0 +1,45 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package browserconfig + +import ( + v1 "github.com/alcounit/browser-controller/pkg/informers/externalversions/browserconfig/v1" + internalinterfaces "github.com/alcounit/browser-controller/pkg/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1 provides access to shared informers for resources in V1. + V1() v1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1 returns a new v1.Interface. +func (g *group) V1() v1.Interface { + return v1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/informers/externalversions/browserconfig/v1/browserconfig.go b/pkg/informers/externalversions/browserconfig/v1/browserconfig.go new file mode 100644 index 0000000..007bf19 --- /dev/null +++ b/pkg/informers/externalversions/browserconfig/v1/browserconfig.go @@ -0,0 +1,101 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1 + +import ( + context "context" + time "time" + + apisbrowserconfigv1 "github.com/alcounit/browser-controller/apis/browserconfig/v1" + clientset "github.com/alcounit/browser-controller/pkg/clientset" + internalinterfaces "github.com/alcounit/browser-controller/pkg/informers/externalversions/internalinterfaces" + browserconfigv1 "github.com/alcounit/browser-controller/pkg/listers/browserconfig/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// BrowserConfigInformer provides access to a shared informer and lister for +// BrowserConfigs. +type BrowserConfigInformer interface { + Informer() cache.SharedIndexInformer + Lister() browserconfigv1.BrowserConfigLister +} + +type browserConfigInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewBrowserConfigInformer constructs a new informer for BrowserConfig type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewBrowserConfigInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredBrowserConfigInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredBrowserConfigInformer constructs a new informer for BrowserConfig type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredBrowserConfigInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.BrowserconfigV1().BrowserConfigs(namespace).List(context.Background(), options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.BrowserconfigV1().BrowserConfigs(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.BrowserconfigV1().BrowserConfigs(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.BrowserconfigV1().BrowserConfigs(namespace).Watch(ctx, options) + }, + }, + &apisbrowserconfigv1.BrowserConfig{}, + resyncPeriod, + indexers, + ) +} + +func (f *browserConfigInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredBrowserConfigInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *browserConfigInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisbrowserconfigv1.BrowserConfig{}, f.defaultInformer) +} + +func (f *browserConfigInformer) Lister() browserconfigv1.BrowserConfigLister { + return browserconfigv1.NewBrowserConfigLister(f.Informer().GetIndexer()) +} diff --git a/pkg/informers/externalversions/browserconfig/v1/interface.go b/pkg/informers/externalversions/browserconfig/v1/interface.go new file mode 100644 index 0000000..4e524e9 --- /dev/null +++ b/pkg/informers/externalversions/browserconfig/v1/interface.go @@ -0,0 +1,44 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1 + +import ( + internalinterfaces "github.com/alcounit/browser-controller/pkg/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // BrowserConfigs returns a BrowserConfigInformer. + BrowserConfigs() BrowserConfigInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// BrowserConfigs returns a BrowserConfigInformer. +func (v *version) BrowserConfigs() BrowserConfigInformer { + return &browserConfigInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/informers/externalversions/factory.go b/pkg/informers/externalversions/factory.go index 85c5087..ae64bd4 100644 --- a/pkg/informers/externalversions/factory.go +++ b/pkg/informers/externalversions/factory.go @@ -24,6 +24,7 @@ import ( clientset "github.com/alcounit/browser-controller/pkg/clientset" browser "github.com/alcounit/browser-controller/pkg/informers/externalversions/browser" + browserconfig "github.com/alcounit/browser-controller/pkg/informers/externalversions/browserconfig" internalinterfaces "github.com/alcounit/browser-controller/pkg/informers/externalversions/internalinterfaces" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -253,9 +254,14 @@ type SharedInformerFactory interface { // client. InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer - Selenosis() browser.Interface + Browser() browser.Interface + Browserconfig() browserconfig.Interface } -func (f *sharedInformerFactory) Selenosis() browser.Interface { +func (f *sharedInformerFactory) Browser() browser.Interface { return browser.New(f, f.namespace, f.tweakListOptions) } + +func (f *sharedInformerFactory) Browserconfig() browserconfig.Interface { + return browserconfig.New(f, f.namespace, f.tweakListOptions) +} diff --git a/pkg/informers/externalversions/generic.go b/pkg/informers/externalversions/generic.go index 4563452..a8e6328 100644 --- a/pkg/informers/externalversions/generic.go +++ b/pkg/informers/externalversions/generic.go @@ -21,6 +21,7 @@ import ( fmt "fmt" v1 "github.com/alcounit/browser-controller/apis/browser/v1" + browserconfigv1 "github.com/alcounit/browser-controller/apis/browserconfig/v1" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) @@ -51,9 +52,13 @@ func (f *genericInformer) Lister() cache.GenericLister { // TODO extend this to unknown resources with a client pool func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { - // Group=selenosis.io, Version=v1 + // Group=browser.selenosis.io, Version=v1 case v1.SchemeGroupVersion.WithResource("browsers"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Selenosis().V1().Browsers().Informer()}, nil + return &genericInformer{resource: resource.GroupResource(), informer: f.Browser().V1().Browsers().Informer()}, nil + + // Group=browserconfig.selenosis.io, Version=v1 + case browserconfigv1.SchemeGroupVersion.WithResource("browserconfigs"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Browserconfig().V1().BrowserConfigs().Informer()}, nil } diff --git a/pkg/listers/browserconfig/v1/browserconfig.go b/pkg/listers/browserconfig/v1/browserconfig.go new file mode 100644 index 0000000..922323b --- /dev/null +++ b/pkg/listers/browserconfig/v1/browserconfig.go @@ -0,0 +1,69 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1 + +import ( + browserconfigv1 "github.com/alcounit/browser-controller/apis/browserconfig/v1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// BrowserConfigLister helps list BrowserConfigs. +// All objects returned here must be treated as read-only. +type BrowserConfigLister interface { + // List lists all BrowserConfigs in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*browserconfigv1.BrowserConfig, err error) + // BrowserConfigs returns an object that can list and get BrowserConfigs. + BrowserConfigs(namespace string) BrowserConfigNamespaceLister + BrowserConfigListerExpansion +} + +// browserConfigLister implements the BrowserConfigLister interface. +type browserConfigLister struct { + listers.ResourceIndexer[*browserconfigv1.BrowserConfig] +} + +// NewBrowserConfigLister returns a new BrowserConfigLister. +func NewBrowserConfigLister(indexer cache.Indexer) BrowserConfigLister { + return &browserConfigLister{listers.New[*browserconfigv1.BrowserConfig](indexer, browserconfigv1.Resource("browserconfig"))} +} + +// BrowserConfigs returns an object that can list and get BrowserConfigs. +func (s *browserConfigLister) BrowserConfigs(namespace string) BrowserConfigNamespaceLister { + return browserConfigNamespaceLister{listers.NewNamespaced[*browserconfigv1.BrowserConfig](s.ResourceIndexer, namespace)} +} + +// BrowserConfigNamespaceLister helps list and get BrowserConfigs. +// All objects returned here must be treated as read-only. +type BrowserConfigNamespaceLister interface { + // List lists all BrowserConfigs in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*browserconfigv1.BrowserConfig, err error) + // Get retrieves the BrowserConfig from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*browserconfigv1.BrowserConfig, error) + BrowserConfigNamespaceListerExpansion +} + +// browserConfigNamespaceLister implements the BrowserConfigNamespaceLister +// interface. +type browserConfigNamespaceLister struct { + listers.ResourceIndexer[*browserconfigv1.BrowserConfig] +} diff --git a/pkg/listers/browserconfig/v1/expansion_generated.go b/pkg/listers/browserconfig/v1/expansion_generated.go new file mode 100644 index 0000000..33f4809 --- /dev/null +++ b/pkg/listers/browserconfig/v1/expansion_generated.go @@ -0,0 +1,26 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1 + +// BrowserConfigListerExpansion allows custom methods to be added to +// BrowserConfigLister. +type BrowserConfigListerExpansion interface{} + +// BrowserConfigNamespaceListerExpansion allows custom methods to be added to +// BrowserConfigNamespaceLister. +type BrowserConfigNamespaceListerExpansion interface{} diff --git a/store/browserconfig_store.go b/store/browserconfig_store.go index 83c97e1..721d9ff 100644 --- a/store/browserconfig_store.go +++ b/store/browserconfig_store.go @@ -51,8 +51,8 @@ func (s *BrowserConfigStore) Start(ctx context.Context) error { } informer.AddEventHandler(kcache.ResourceEventHandlerFuncs{ - AddFunc: func(obj any) { s.onAddOrUpdate(obj, s.log) }, - UpdateFunc: func(_, newObj any) { s.onAddOrUpdate(newObj, s.log) }, + AddFunc: func(obj any) { s.onAddOrUpdate(nil, obj, s.log) }, + UpdateFunc: func(oldObj, newObj any) { s.onAddOrUpdate(oldObj, newObj, s.log) }, DeleteFunc: func(obj any) { s.onDelete(obj, s.log) }, }) @@ -66,32 +66,48 @@ func (s *BrowserConfigStore) Start(ctx context.Context) error { return nil } -func (s *BrowserConfigStore) onAddOrUpdate(obj any, log logr.Logger) { - var bc *configv1.BrowserConfig - switch t := obj.(type) { +func (s *BrowserConfigStore) onAddOrUpdate(oldObj, newObj any, log logr.Logger) { + var old *configv1.BrowserConfig + switch t := oldObj.(type) { + case nil: case *configv1.BrowserConfig: - bc = t + old = t case kcache.DeletedFinalStateUnknown: if v, ok := t.Obj.(*configv1.BrowserConfig); ok { - bc = v + old = v + } + default: + } + + var new *configv1.BrowserConfig + switch t := newObj.(type) { + case *configv1.BrowserConfig: + new = t + case kcache.DeletedFinalStateUnknown: + if v, ok := t.Obj.(*configv1.BrowserConfig); ok { + new = v } default: return } - if bc == nil { + if new == nil { + return + } + + if old != nil && old.GetResourceVersion() == new.GetResourceVersion() { return } s.mu.Lock() defer s.mu.Unlock() - bcCopy := bc.DeepCopy() - bcCopy.Spec.MergeWithTemplate() + copy := new.DeepCopy() + copy.Spec.MergeWithTemplate() - for browserName, versions := range bcCopy.Spec.Browsers { + for browserName, versions := range copy.Spec.Browsers { for version, cfg := range versions { - key := keyFor(bcCopy.Namespace, browserName, version) + key := keyFor(copy.Namespace, browserName, version) s.config[key] = cfg log.Info("BrowserConfig added/updated", "key", key) } diff --git a/store/browserconfig_store_test.go b/store/browserconfig_store_test.go index e5aa9d7..2caf2a7 100644 --- a/store/browserconfig_store_test.go +++ b/store/browserconfig_store_test.go @@ -61,7 +61,7 @@ func TestBrowserConfigStoreOnAddOrUpdateMergesAndStores(t *testing.T) { } store := NewBrowserConfigStore() - store.onAddOrUpdate(bc, logr.Discard()) + store.onAddOrUpdate(nil, bc, logr.Discard()) cfg, ok := store.Get("ns", "CHROME", "144.0") if !ok || cfg == nil { @@ -94,7 +94,7 @@ func TestBrowserConfigStoreOnAddOrUpdateDeepCopyIsolated(t *testing.T) { } store := NewBrowserConfigStore() - store.onAddOrUpdate(bc, logr.Discard()) + store.onAddOrUpdate(nil, bc, logr.Discard()) // Mutate original after add. bc.Spec.Browsers["Chrome"]["144.0"].ImagePullPolicy = corev1.PullNever @@ -122,7 +122,7 @@ func TestBrowserConfigStoreOnAddOrUpdateDeletedFinalStateUnknown(t *testing.T) { } store := NewBrowserConfigStore() - store.onAddOrUpdate(cache.DeletedFinalStateUnknown{Obj: bc}, logr.Discard()) + store.onAddOrUpdate(nil, cache.DeletedFinalStateUnknown{Obj: bc}, logr.Discard()) if _, ok := store.Get("ns", "firefox", "120.0"); !ok { t.Fatalf("expected config to be stored from DeletedFinalStateUnknown") @@ -131,17 +131,47 @@ func TestBrowserConfigStoreOnAddOrUpdateDeletedFinalStateUnknown(t *testing.T) { func TestBrowserConfigStoreOnAddOrUpdateDeletedFinalStateUnknownNilObj(t *testing.T) { store := NewBrowserConfigStore() - store.onAddOrUpdate(cache.DeletedFinalStateUnknown{Obj: (*configv1.BrowserConfig)(nil)}, logr.Discard()) + store.onAddOrUpdate(nil, cache.DeletedFinalStateUnknown{Obj: (*configv1.BrowserConfig)(nil)}, logr.Discard()) } func TestBrowserConfigStoreOnAddOrUpdateIgnoresUnknownType(t *testing.T) { store := NewBrowserConfigStore() - store.onAddOrUpdate("not-a-config", logr.Discard()) + store.onAddOrUpdate(nil, "not-a-config", logr.Discard()) if _, ok := store.Get("ns", "chrome", "144.0"); ok { t.Fatalf("expected no configs to be stored") } } +func TestBrowserConfigStoreOnAddOrUpdateSkipsSameResourceVersion(t *testing.T) { + store := NewBrowserConfigStore() + + oldObj := &configv1.BrowserConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cfg", + Namespace: "ns", + ResourceVersion: "10", + }, + Spec: configv1.BrowserConfigSpec{ + Browsers: map[string]map[string]*configv1.BrowserVersionConfigSpec{ + "Chrome": {"144.0": {Image: "old"}}, + }, + }, + } + newObj := oldObj.DeepCopy() + newObj.Spec.Browsers["Chrome"]["144.0"].Image = "new" + + store.onAddOrUpdate(nil, oldObj, logr.Discard()) + store.onAddOrUpdate(oldObj, newObj, logr.Discard()) + + cfg, ok := store.Get("ns", "chrome", "144.0") + if !ok || cfg == nil { + t.Fatalf("expected config to be stored") + } + if cfg.Image != "old" { + t.Fatalf("expected update with same resourceVersion to be skipped, got image %q", cfg.Image) + } +} + func TestBrowserConfigStoreOnDeleteRemovesKeys(t *testing.T) { bc := &configv1.BrowserConfig{ ObjectMeta: metav1.ObjectMeta{ @@ -157,7 +187,7 @@ func TestBrowserConfigStoreOnDeleteRemovesKeys(t *testing.T) { } store := NewBrowserConfigStore() - store.onAddOrUpdate(bc, logr.Discard()) + store.onAddOrUpdate(nil, bc, logr.Discard()) store.onDelete(bc, logr.Discard()) @@ -183,7 +213,7 @@ func TestBrowserConfigStoreOnDeleteDeletedFinalStateUnknown(t *testing.T) { } store := NewBrowserConfigStore() - store.onAddOrUpdate(bc, logr.Discard()) + store.onAddOrUpdate(nil, bc, logr.Discard()) store.onDelete(cache.DeletedFinalStateUnknown{Obj: bc}, logr.Discard()) if _, ok := store.Get("ns", "safari", "17.0"); ok {