├── assets └── logo.png ├── pkg ├── operator │ ├── apis │ │ └── rusi │ │ │ ├── register.go │ │ │ └── v1alpha1 │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ ├── componentTypes.go │ │ │ └── configurationTypes.go │ ├── tools │ │ ├── generate_kube_crd.mk │ │ └── boilerplate.go.txt │ ├── client │ │ ├── clientset │ │ │ └── versioned │ │ │ │ ├── doc.go │ │ │ │ ├── fake │ │ │ │ ├── doc.go │ │ │ │ ├── register.go │ │ │ │ └── clientset_generated.go │ │ │ │ ├── typed │ │ │ │ └── rusi │ │ │ │ │ └── v1alpha1 │ │ │ │ │ ├── fake │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── fake_rusi_client.go │ │ │ │ │ ├── fake_component.go │ │ │ │ │ └── fake_configuration.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── generated_expansion.go │ │ │ │ │ └── rusi_client.go │ │ │ │ ├── scheme │ │ │ │ ├── doc.go │ │ │ │ └── register.go │ │ │ │ └── clientset.go │ │ ├── listers │ │ │ └── rusi │ │ │ │ └── v1alpha1 │ │ │ │ ├── expansion_generated.go │ │ │ │ ├── component.go │ │ │ │ └── configuration.go │ │ └── informers │ │ │ └── externalversions │ │ │ ├── internalinterfaces │ │ │ └── factory_interfaces.go │ │ │ ├── rusi │ │ │ ├── interface.go │ │ │ └── v1alpha1 │ │ │ │ ├── interface.go │ │ │ │ ├── component.go │ │ │ │ └── configuration.go │ │ │ └── generic.go │ ├── server.go │ └── client.go ├── messaging │ ├── extractors.go │ ├── publisher.go │ ├── pubsub.go │ ├── subscriber.go │ ├── jetstream │ │ └── options.go │ ├── pipeline.go │ ├── nats │ │ └── options.go │ ├── serdes │ │ └── serdes.go │ ├── types.go │ └── inmemory.go ├── modes │ └── modes.go ├── custom-resource │ ├── components │ │ ├── loader │ │ │ ├── loader.go │ │ │ ├── local.go │ │ │ └── local_test.go │ │ ├── versioning.go │ │ ├── types.go │ │ ├── pubsub │ │ │ └── registry.go │ │ └── middleware │ │ │ └── registry.go │ └── configuration │ │ ├── loader │ │ ├── loader.go │ │ ├── local.go │ │ └── local_test.go │ │ ├── configuration.go │ │ ├── configuration_test.go │ │ └── types.go ├── api │ └── runtime │ │ ├── api.go │ │ ├── api_errors.go │ │ └── test_api.go ├── utils │ ├── env.go │ └── strings.go ├── healthcheck │ ├── types.go │ └── health.go ├── runtime │ ├── options.go │ ├── service │ │ └── subscriber.go │ ├── config.go │ └── runtime.go ├── middleware │ ├── metrics.go │ └── tracing.go ├── injector │ ├── config.go │ └── validation.go └── proto │ └── runtime │ └── v1 │ └── rusi_grpc.pb.go ├── helm ├── charts │ ├── rusi_sidecar_injector │ │ ├── templates │ │ │ ├── ServiceAccount.yaml │ │ │ ├── pdb.yaml │ │ │ ├── sidecar_injector_service.yaml │ │ │ ├── ClusterRoleBinding.yaml │ │ │ ├── sidecar_injector_webhook_config.yaml │ │ │ └── sidecar_injector_deployment.yaml │ │ ├── Chart.yaml │ │ ├── .helmignore │ │ └── values.yaml │ └── rusi_operator │ │ ├── Chart.yaml │ │ ├── templates │ │ ├── rusi_operator_service.yaml │ │ └── rusi_operator_deployment.yaml │ │ ├── .helmignore │ │ └── values.yaml ├── values.yaml ├── Chart.yaml ├── .helmignore ├── templates │ └── _helpers.tpl └── crds │ ├── rusi.io_components.yaml │ └── rusi.io_configurations.yaml ├── examples └── components │ ├── comp-uppercase-pubsub.yaml │ ├── comp-jetstream.yaml │ ├── config-node-pipeline.yaml │ └── comp-nats.yaml ├── .gitignore ├── docker ├── Dockerfile └── docker.mk ├── internal ├── tracing │ ├── Readme.md │ └── tracing.go ├── version │ └── version.go ├── diagnostics │ ├── server.go │ └── diagnostics.go ├── metrics │ ├── prometheus.go │ └── metrics.go └── kube │ └── client.go ├── .github ├── release-drafter.yml └── workflows │ ├── release-drafter.yml │ ├── ci.yml │ └── release.yml ├── cmd ├── operator │ └── operator.go ├── injector │ └── injector.go └── rusid │ ├── components.go │ └── sidecar.go ├── proto ├── operator │ └── v1 │ │ └── rusi_operator.proto └── runtime │ └── v1 │ └── rusi.proto ├── GenerateCRD.md ├── .vscode └── launch.json ├── Makefile ├── README.md └── go.mod /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osstotalsoft/rusi/HEAD/assets/logo.png -------------------------------------------------------------------------------- /pkg/operator/apis/rusi/register.go: -------------------------------------------------------------------------------- 1 | package rusi 2 | 3 | const ( 4 | GroupName = "rusi.io" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/operator/apis/rusi/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +groupName=rusi.io 3 | 4 | package v1alpha1 5 | -------------------------------------------------------------------------------- /pkg/messaging/extractors.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | func ExtractTopicFromEnvelope(msg *MessageEnvelope) string { 4 | return "" 5 | } 6 | -------------------------------------------------------------------------------- /pkg/messaging/publisher.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | type Publisher interface { 4 | Publish(topic string, env *MessageEnvelope) error 5 | } 6 | -------------------------------------------------------------------------------- /helm/charts/rusi_sidecar_injector/templates/ServiceAccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: rusi-operator 5 | -------------------------------------------------------------------------------- /helm/charts/rusi_operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Rusi Kubernetes Operator 4 | name: rusi_operator 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /pkg/messaging/pubsub.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | type PubSub interface { 4 | Publisher 5 | Subscriber 6 | Init(properties map[string]string) error 7 | Close() error 8 | } 9 | -------------------------------------------------------------------------------- /pkg/messaging/subscriber.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | type Subscriber interface { 4 | Subscribe(topic string, handler Handler, options *SubscriptionOptions) (CloseFunc, error) 5 | } 6 | -------------------------------------------------------------------------------- /helm/charts/rusi_sidecar_injector/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for the Rusi sidecar injector 4 | name: rusi_sidecar_injector 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /pkg/modes/modes.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | // RusiMode is the runtime mode for Rusi. 4 | type RusiMode string 5 | 6 | const ( 7 | KubernetesMode RusiMode = "kubernetes" 8 | StandaloneMode RusiMode = "standalone" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/custom-resource/components/loader/loader.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "context" 5 | "rusi/pkg/custom-resource/components" 6 | ) 7 | 8 | type ComponentsLoader func(ctx context.Context) (<-chan components.Spec, error) 9 | -------------------------------------------------------------------------------- /pkg/custom-resource/configuration/loader/loader.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "context" 5 | "rusi/pkg/custom-resource/configuration" 6 | ) 7 | 8 | type ConfigurationLoader func(ctx context.Context) (<-chan configuration.Spec, error) 9 | -------------------------------------------------------------------------------- /helm/charts/rusi_sidecar_injector/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: rusi-sidecar-injector 5 | spec: 6 | minAvailable: 1 7 | selector: 8 | matchLabels: 9 | app: rusi-sidecar-injector 10 | -------------------------------------------------------------------------------- /examples/components/comp-uppercase-pubsub.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rusi.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: pubsub-uppercase 5 | spec: 6 | version: v1 7 | type: middleware.pubsub.uppercase 8 | metadata: 9 | - name: scopes 10 | value: "*" -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | registry: ghcr.io/osstotalsoft/rusi 3 | tag: "0.0.29" 4 | dnsSuffix: ".cluster.local" 5 | imagePullPolicy: IfNotPresent 6 | imagePullSecrets: "registrykey" 7 | nodeSelector: {} 8 | prometheus: 9 | enabled: false 10 | port: 9090 11 | -------------------------------------------------------------------------------- /pkg/custom-resource/configuration/configuration.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | func IsFeatureEnabled(features []FeatureSpec, target Feature) bool { 4 | for _, feature := range features { 5 | if feature.Name == target { 6 | return feature.Enabled 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # IDE settings 15 | .idea/** 16 | 17 | dist/** -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest as alpine 2 | RUN apk add -U --no-cache ca-certificates 3 | # current directory must be ./dist 4 | 5 | FROM gcr.io/distroless/static:nonroot 6 | ARG PKG_FILES 7 | WORKDIR / 8 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 9 | COPY /$PKG_FILES / 10 | -------------------------------------------------------------------------------- /helm/charts/rusi_operator/templates/rusi_operator_service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: rusi-api 5 | spec: 6 | selector: 7 | app: rusi-operator 8 | ports: 9 | - protocol: TCP 10 | port: {{ .Values.ports.port }} 11 | targetPort: {{ .Values.ports.targetPort }} -------------------------------------------------------------------------------- /pkg/api/runtime/api.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "rusi/pkg/messaging" 6 | ) 7 | 8 | type Api interface { 9 | Serve(ctx context.Context) error 10 | Refresh() error 11 | SetPublishHandler(messaging.PublishRequestHandler) 12 | SetSubscribeHandler(messaging.SubscribeRequestHandler) 13 | } 14 | -------------------------------------------------------------------------------- /helm/charts/rusi_sidecar_injector/templates/sidecar_injector_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: rusi-sidecar-injector 5 | spec: 6 | type: ClusterIP 7 | ports: 8 | - port: 443 9 | targetPort: https 10 | protocol: TCP 11 | name: https 12 | selector: 13 | app: rusi-sidecar-injector 14 | -------------------------------------------------------------------------------- /internal/tracing/Readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## Tracing config 3 | * `collectorEnpoint` (string) the Telemetry Collector Endpoint. 4 | * `propagator` (string) w3c | jaeger 5 | 6 | Example: 7 | 8 | ```yaml 9 | telemetry: 10 | collectorEnpoint: opentelemetry-opentelemetry-collector.global-infra:4317 11 | tracing: 12 | propagator: w3c # w3c | jaeger 13 | ``` -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "0.0.1" 3 | description: A Helm chart for Rusi on Kubernetes 4 | name: rusi 5 | version: 0.0.1 6 | dependencies: 7 | - name: rusi_sidecar_injector 8 | version: "0.0.1" 9 | repository: "file://rusi_sidecar_injector" 10 | - name: rusi_operator 11 | version: "0.0.1" 12 | repository: "file://rusi_operator" 13 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: v$NEXT_PATCH_VERSION 2 | tag-template: v$NEXT_PATCH_VERSION 3 | categories: 4 | - title: 🚀 Features 5 | label: feature 6 | - title: 🐛 Bug Fixes 7 | label: fix 8 | - title: 🛠️ Maintenance 9 | label: chore 10 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 11 | template: | 12 | ## Changes 13 | 14 | $CHANGES 15 | -------------------------------------------------------------------------------- /pkg/messaging/jetstream/options.go: -------------------------------------------------------------------------------- 1 | package jetstream 2 | 3 | import "time" 4 | 5 | type options struct { 6 | natsURL string 7 | durableSubscriptionName string 8 | deliverNew string 9 | deliverAll string 10 | connectWait time.Duration 11 | ackWaitTime time.Duration 12 | maxInFlight int 13 | } 14 | -------------------------------------------------------------------------------- /cmd/operator/operator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "k8s.io/klog/v2" 6 | "rusi/internal/kube" 7 | "rusi/pkg/operator" 8 | ) 9 | 10 | func main() { 11 | //https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md 12 | klog.InitFlags(nil) 13 | kube.InitFlags(nil) 14 | defer klog.Flush() 15 | flag.Parse() 16 | 17 | operator.Run() 18 | } 19 | -------------------------------------------------------------------------------- /pkg/custom-resource/components/versioning.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "strings" 4 | 5 | const ( 6 | // Unstable version (v0). 7 | UnstableVersion = "v0" 8 | 9 | // First stable version (v1). 10 | FirstStableVersion = "v1" 11 | ) 12 | 13 | func IsInitialVersion(version string) bool { 14 | v := strings.ToLower(version) 15 | return v == "" || v == UnstableVersion || v == FirstStableVersion 16 | } 17 | -------------------------------------------------------------------------------- /examples/components/comp-jetstream.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rusi.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: jetstream-pubsub 5 | spec: 6 | type: pubsub.jetstream 7 | version: v1 8 | metadata: 9 | - name: natsURL 10 | value: "nats://linux-ts1858:4222" 11 | - name: connectWait 12 | value: 10s 13 | - name: ackWaitTime 14 | value: 50s 15 | - name: maxInFlight 16 | value: '1' 17 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | 24 | packages/ 25 | -------------------------------------------------------------------------------- /helm/charts/rusi_operator/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /helm/charts/rusi_sidecar_injector/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /examples/components/config-node-pipeline.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rusi.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: node-pipeline-config 5 | spec: 6 | metric: 7 | enabled: true 8 | telemetry: 9 | collectorEndpoint: 'linux-ts1858:4317' 10 | tracing: 11 | propagator: w3c # w3c | b3 12 | # subscriberPipeline: 13 | # handlers: 14 | # - name: pubsub-uppercase 15 | # type: middleware.pubsub.uppercase 16 | pubSub: 17 | name: jetstream-pubsub 18 | -------------------------------------------------------------------------------- /pkg/messaging/pipeline.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | type Middleware func(next Handler) Handler 4 | 5 | type Pipeline struct { 6 | Middlewares []Middleware 7 | } 8 | 9 | func (p Pipeline) Build(handler Handler) Handler { 10 | for i := len(p.Middlewares) - 1; i >= 0; i-- { 11 | handler = p.Middlewares[i](handler) 12 | } 13 | return handler 14 | } 15 | 16 | func (p *Pipeline) UseMiddleware(middleware Middleware) { 17 | p.Middlewares = append(p.Middlewares, middleware) 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.25 21 | 22 | - name: Build 23 | run: make build-linux 24 | 25 | - name: Test 26 | run: make testV 27 | -------------------------------------------------------------------------------- /pkg/utils/env.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | const ( 4 | // RusiGRPCPort is the rusi api grpc port. 5 | RusiGRPCPort string = "RUSI_GRPC_PORT" 6 | // RusiGRPCHost is the rusi api grpc host. 7 | RusiGRPCHost string = "RUSI_GRPC_HOST" 8 | // RusiMetricsPort is the rusi metrics port. 9 | RusiMetricsPort string = "RUSI_METRICS_PORT" 10 | // RusiProfilePort is the rusi performance profiling port. 11 | RusiProfilePort string = "RUSI_PROFILE_PORT" 12 | // AppID is the ID of the application. 13 | AppID string = "APP_ID" 14 | ) 15 | -------------------------------------------------------------------------------- /helm/charts/rusi_operator/values.yaml: -------------------------------------------------------------------------------- 1 | logLevel: 4 2 | 3 | # Specify full docker image name including registry url to use a custom operator service image 4 | # Otherwise, helm chart will use {{ .Values.global.registry }}/rusi:{{ .Values.global.tag }} 5 | image: 6 | name: "" 7 | 8 | nameOverride: "" 9 | fullnameOverride: "" 10 | 11 | runAsNonRoot: true 12 | 13 | ports: 14 | protocol: TCP 15 | port: 80 16 | targetPort: 6500 17 | 18 | resources: {} 19 | 20 | debug: 21 | enabled: false 22 | port: 40000 23 | initialDelaySeconds: 30000 -------------------------------------------------------------------------------- /proto/operator/v1/rusi_operator.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package rusi.proto.operator.v1; 4 | 5 | option go_package = "pkg/proto/operator/v1"; 6 | 7 | service RusiOperator { 8 | rpc WatchConfiguration(WatchConfigurationRequest) returns (stream GenericItem); 9 | rpc WatchComponents(WatchComponentsRequest) returns (stream GenericItem); 10 | } 11 | 12 | message WatchConfigurationRequest { 13 | string config_name = 1; 14 | string namespace = 2; 15 | } 16 | message WatchComponentsRequest { 17 | string namespace = 1; 18 | } 19 | message GenericItem { 20 | bytes data = 1; 21 | } -------------------------------------------------------------------------------- /pkg/operator/tools/generate_kube_crd.mk: -------------------------------------------------------------------------------- 1 | #CODE_GENERATOR_DIR?=/d/GoProjects/pkg/mod/k8s.io/code-generator\@v0.22.2 2 | CODE_GENERATOR_DIR?=~/go/pkg/mod/k8s.io/code-generator@v0.22.2 3 | TOOLS_DIR?=pkg/operator/tools 4 | 5 | generate-apis: 6 | $(CODE_GENERATOR_DIR)/generate-groups.sh all rusi/pkg/operator/client rusi/pkg/operator/apis \ 7 | rusi:v1alpha1 \ 8 | -h $(TOOLS_DIR)/boilerplate.go.txt 9 | 10 | cp -rf ./rusi/pkg/ ./ 11 | rm -rf ./rusi/ 12 | 13 | #$(TOOLS_DIR)/update-codegen.sh 14 | 15 | generate-crd: 16 | controller-gen crd paths="rusi/pkg/operator/apis/..." +output:dir=helm/crds -------------------------------------------------------------------------------- /examples/components/comp-nats.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rusi.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: natsstreaming-pubsub 5 | spec: 6 | type: pubsub.natsstreaming 7 | version: v1 8 | metadata: 9 | - name: natsURL 10 | value: "nats://kube-worker1:31291" 11 | - name: natsStreamingClusterID 12 | value: faas-cluster 13 | - name: subscriptionType 14 | value: queue 15 | - name: connectWait 16 | value: 10s 17 | - name: ackWaitTime 18 | value: 50s 19 | - name: maxInFlight 20 | value: '1' 21 | - name: durableSubscriptionName 22 | value: durable 23 | -------------------------------------------------------------------------------- /pkg/api/runtime/api_errors.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | const ( 4 | ErrPubsubNotConfigured = "no pubsub is configured" 5 | ErrPubsubEmpty = "pubsub name is empty" 6 | ErrPubsubNotFound = "pubsub %s not found" 7 | ErrTopicEmpty = "topic is empty in pubsub %s" 8 | ErrPubsubCloudEventsSer = "error when marshalling cloud event envelope for topic %s pubsub %s: %s" 9 | ErrPubsubPublishMessage = "error when publish to topic %s in pubsub %s: %s" 10 | ErrPubsubForbidden = "topic %s is not allowed for app id %s" 11 | ErrPubsubCloudEventCreation = "cannot create cloudevent: %s" 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/operator/tools/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /pkg/messaging/nats/options.go: -------------------------------------------------------------------------------- 1 | package natsstreaming 2 | 3 | import "time" 4 | 5 | type options struct { 6 | natsURL string 7 | natsStreamingClusterID string 8 | subscriptionType string 9 | natsQueueGroupName string 10 | durableSubscriptionName string 11 | startAtSequence uint64 12 | startWithLastReceived string 13 | deliverNew string 14 | deliverAll string 15 | startAtTimeDelta time.Duration 16 | connectWait time.Duration 17 | startAtTime string 18 | startAtTimeFormat string 19 | ackWaitTime time.Duration 20 | maxInFlight uint64 21 | } 22 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Values for these are injected by the build. 4 | var ( 5 | version = "edge" 6 | 7 | gitcommit, gitversion string 8 | ) 9 | 10 | // Version returns the Rusi version. This is either a semantic version 11 | // number or else, in the case of unreleased code, the string "edge". 12 | func Version() string { 13 | return version 14 | } 15 | 16 | // Commit returns the git commit SHA for the code that Rusi was built from. 17 | func Commit() string { 18 | return gitcommit 19 | } 20 | 21 | // GitVersion returns the git version for the code that Rusi was built from. 22 | func GitVersion() string { 23 | return gitversion 24 | } 25 | -------------------------------------------------------------------------------- /pkg/custom-resource/configuration/configuration_test.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestFeatureEnabled(t *testing.T) { 9 | t.Run("Test feature enabled is correct", func(t *testing.T) { 10 | features := []FeatureSpec{ 11 | { 12 | Name: "testEnabled", 13 | Enabled: true, 14 | }, 15 | { 16 | Name: "testDisabled", 17 | Enabled: false, 18 | }, 19 | } 20 | assert.True(t, IsFeatureEnabled(features, "testEnabled")) 21 | assert.False(t, IsFeatureEnabled(features, "testDisabled")) 22 | assert.False(t, IsFeatureEnabled(features, "testMissing")) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /helm/charts/rusi_sidecar_injector/values.yaml: -------------------------------------------------------------------------------- 1 | logLevel: 4 2 | 3 | # Specify full docker image name including registry url to use the custom side car image 4 | # Otherwise, helm chart will use {{ .Values.global.registry }}/rusid:{{ .Values.global.tag }} 5 | image: 6 | name: "" 7 | 8 | # Specify full docker image name including registry url to use a custom injector service image 9 | # Otherwise, helm chart will use {{ .Values.global.registry }}/rusi:{{ .Values.global.tag }} 10 | injectorImage: 11 | name: "" 12 | 13 | webhookFailurePolicy: Ignore 14 | webhookHostNetwork: false 15 | sidecarImagePullPolicy: IfNotPresent 16 | runAsNonRoot: true 17 | resources: {} 18 | kubeClusterDomain: cluster.local 19 | 20 | debug: 21 | enabled: false 22 | port: 40000 23 | initialDelaySeconds: 30000 24 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated clientset. 20 | package versioned 21 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated fake clientset. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // Package fake has the automatically generated clients. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package contains the scheme of the automatically generated clientset. 20 | package scheme 21 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated typed clients. 20 | package v1alpha1 21 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | type ComponentExpansion interface{} 22 | 23 | type ConfigurationExpansion interface{} 24 | -------------------------------------------------------------------------------- /pkg/utils/strings.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "strings" 6 | ) 7 | 8 | // add env-vars from annotations. 9 | func ParseEnvString(envStr string) []corev1.EnvVar { 10 | envVars := make([]corev1.EnvVar, 0) 11 | envPairs := strings.Split(envStr, ",") 12 | 13 | for _, value := range envPairs { 14 | pair := strings.Split(strings.TrimSpace(value), "=") 15 | 16 | if len(pair) != 2 { 17 | continue 18 | } 19 | 20 | envVars = append(envVars, corev1.EnvVar{ 21 | Name: pair[0], 22 | Value: pair[1], 23 | }) 24 | } 25 | 26 | return envVars 27 | } 28 | 29 | // StringSliceContains return true if an array containe the "str" string. 30 | func StringSliceContains(needle string, haystack []string) bool { 31 | for _, item := range haystack { 32 | if item == needle { 33 | return true 34 | } 35 | } 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /GenerateCRD.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | Use **Windows Subsystem for Linux** (Ubuntu) 3 | 4 | 1. Install go v1.17 from [here](https://golang.org/doc/install) 5 | 6 | 2. Install make 7 | ```bash 8 | apt install make 9 | ``` 10 | 3. Install the kubernetes code generator: 11 | ```bash 12 | go install k8s.io/code-generator@v0.22.2 13 | sudo chmod 777 ~/go/pkg/mod/k8s.io/code-generator@v0.22.2/generate-groups.sh 14 | ``` 15 | 16 | 4. Install controller-gen https://book.kubebuilder.io/reference/controller-gen.html 17 | ```shell 18 | go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest 19 | ``` 20 | 21 | 22 | ## Generate apis 23 | Go to rusi folder (repository root) and run the following make task: 24 | ```bash 25 | make generate-apis 26 | ``` 27 | 28 | ## Generate crd 29 | Go to rusi folder (repository root) and run the following make task: 30 | ```bash 31 | make generate-crd 32 | ``` -------------------------------------------------------------------------------- /pkg/healthcheck/types.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import "context" 4 | 5 | type HealthChecker interface { 6 | IsHealthy(ctx context.Context) HealthResult 7 | } 8 | 9 | // CheckerFunc is a convenience type to create functions that implement the HealthChecker interface. 10 | type CheckerFunc func(ctx context.Context) HealthResult 11 | 12 | // IsHealthy Implements the HealthChecker interface to allow for any func() HealthResult method 13 | // to be passed as a HealthChecker 14 | func (c CheckerFunc) IsHealthy(ctx context.Context) HealthResult { 15 | return c(ctx) 16 | } 17 | 18 | type HealthStatus int 19 | 20 | const ( 21 | Unhealthy HealthStatus = 0 22 | Degraded = 1 23 | Healthy = 2 24 | ) 25 | 26 | type HealthResult struct { 27 | Status HealthStatus 28 | Description string 29 | } 30 | 31 | var HealthyResult = HealthResult{Status: Healthy} 32 | -------------------------------------------------------------------------------- /internal/diagnostics/server.go: -------------------------------------------------------------------------------- 1 | package diagnostics 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "k8s.io/klog/v2" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func Run(ctx context.Context, port int, router http.Handler) error { 12 | srv := &http.Server{ 13 | Addr: fmt.Sprintf(":%d", port), 14 | Handler: router, 15 | } 16 | 17 | doneCh := make(chan struct{}) 18 | 19 | go func() { 20 | select { 21 | case <-ctx.Done(): 22 | klog.Info("Diagnostics server is shutting down") 23 | shutdownCtx, cancel := context.WithTimeout( 24 | context.Background(), 25 | time.Second*5, 26 | ) 27 | defer cancel() 28 | srv.Shutdown(shutdownCtx) // nolint: errcheck 29 | case <-doneCh: 30 | } 31 | }() 32 | 33 | klog.Infof("Diagnostics server is listening on %s", srv.Addr) 34 | err := srv.ListenAndServe() 35 | klog.Info("Diagnostics server was closed") 36 | close(doneCh) 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /pkg/api/runtime/test_api.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "rusi/pkg/messaging" 6 | ) 7 | 8 | type TestApi struct { 9 | PublishRequestHandler messaging.PublishRequestHandler 10 | SubscribeRequestHandler messaging.SubscribeRequestHandler 11 | RefreshChan chan bool 12 | } 13 | 14 | func NewTestApi() *TestApi { 15 | return &TestApi{RefreshChan: make(chan bool)} 16 | } 17 | 18 | func (TestApi) Serve(ctx context.Context) error { 19 | return nil 20 | } 21 | 22 | func (d *TestApi) Refresh() error { 23 | d.RefreshChan <- true 24 | return nil 25 | } 26 | 27 | func (d *TestApi) SetPublishHandler(publishRequestHandler messaging.PublishRequestHandler) { 28 | d.PublishRequestHandler = publishRequestHandler 29 | } 30 | 31 | func (d *TestApi) SetSubscribeHandler(subscribeRequestHandler messaging.SubscribeRequestHandler) { 32 | d.SubscribeRequestHandler = subscribeRequestHandler 33 | 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run sidecar", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "./cmd/rusid", 13 | "args": ["--app-id", "rusi-vs-code-debug", "--config", "../../examples/components/config-node-pipeline.yaml", "--components-path", "../../examples/components", "--v", "4"] 14 | }, 15 | { 16 | "name": "Test Current File", 17 | "type": "go", 18 | "request": "launch", 19 | "mode": "test", 20 | "program": "${relativeFileDirname}", 21 | "args": [] 22 | } 23 | 24 | ] 25 | } -------------------------------------------------------------------------------- /pkg/runtime/options.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "rusi/pkg/custom-resource/components/middleware" 5 | "rusi/pkg/custom-resource/components/pubsub" 6 | ) 7 | 8 | type ( 9 | // runtimeOpts encapsulates the components to include in the runtime. 10 | runtimeOpts struct { 11 | pubsubs []pubsub.PubSubDefinition 12 | pubsubMiddleware []middleware.Middleware 13 | } 14 | 15 | // Option is a function that customizes the runtime. 16 | Option func(o *runtimeOpts) 17 | ) 18 | 19 | // WithPubSubs adds pubsub store components to the runtime. 20 | func WithPubSubs(pubsubs ...pubsub.PubSubDefinition) Option { 21 | return func(o *runtimeOpts) { 22 | o.pubsubs = append(o.pubsubs, pubsubs...) 23 | } 24 | } 25 | 26 | // WithPubsubMiddleware adds Pubsub middleware components to the runtime. 27 | func WithPubsubMiddleware(middleware ...middleware.Middleware) Option { 28 | return func(o *runtimeOpts) { 29 | o.pubsubMiddleware = append(o.pubsubMiddleware, middleware...) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docker/docker.mk: -------------------------------------------------------------------------------- 1 | # Docker image build and push setting 2 | DOCKER:=docker 3 | DOCKERFILE_DIR?=./docker 4 | 5 | RUSI_SYSTEM_IMAGE_NAME=$(RELEASE_NAME) 6 | RUSI_RUNTIME_IMAGE_NAME=rusid 7 | 8 | # build docker image for linux 9 | BIN_PATH=$(OUT_DIR) 10 | DOCKERFILE:=Dockerfile 11 | 12 | check-docker-env: 13 | ifeq ($(RUSI_REGISTRY),) 14 | $(error RUSI_REGISTRY environment variable must be set) 15 | endif 16 | ifeq ($(RUSI_TAG),) 17 | $(error RUSI_TAG environment variable must be set) 18 | endif 19 | 20 | docker-build: check-docker-env 21 | $(DOCKER) build --build-arg PKG_FILES=* -f $(DOCKERFILE_DIR)/$(DOCKERFILE) $(BIN_PATH)/. -t $(RUSI_REGISTRY)/$(RUSI_SYSTEM_IMAGE_NAME):$(RUSI_TAG) 22 | $(DOCKER) build --build-arg PKG_FILES=$(RUSI_RUNTIME_IMAGE_NAME) -f $(DOCKERFILE_DIR)/$(DOCKERFILE) $(BIN_PATH)/. -t $(RUSI_REGISTRY)/$(RUSI_RUNTIME_IMAGE_NAME):$(RUSI_TAG) 23 | 24 | docker-push: check-docker-env 25 | $(DOCKER) push $(RUSI_REGISTRY)/$(RUSI_SYSTEM_IMAGE_NAME):$(RUSI_TAG) 26 | $(DOCKER) push $(RUSI_REGISTRY)/$(RUSI_RUNTIME_IMAGE_NAME):$(RUSI_TAG) 27 | -------------------------------------------------------------------------------- /pkg/middleware/metrics.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "rusi/internal/metrics" 6 | "rusi/pkg/messaging" 7 | "time" 8 | ) 9 | 10 | func SubscriberMetricsMiddleware() messaging.Middleware { 11 | return func(next messaging.Handler) messaging.Handler { 12 | return func(ctx context.Context, msg *messaging.MessageEnvelope) error { 13 | start := time.Now() 14 | err := next(ctx, msg) 15 | topic := msg.Subject 16 | if topic == "" { 17 | topic = ctx.Value(messaging.TopicKey).(string) 18 | } 19 | 20 | metrics.DefaultPubSubMetrics().RecordSubscriberProcessingTime(ctx, topic, err == nil, time.Since(start)) 21 | return err 22 | } 23 | } 24 | } 25 | 26 | func PublisherMetricsMiddleware() messaging.Middleware { 27 | return func(next messaging.Handler) messaging.Handler { 28 | return func(ctx context.Context, msg *messaging.MessageEnvelope) error { 29 | err := next(ctx, msg) 30 | metrics.DefaultPubSubMetrics().RecordPublishMessage(ctx, msg.Subject, err == nil) 31 | return err 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cmd/injector/injector.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "k8s.io/klog/v2" 7 | "log" 8 | "net/http" 9 | "rusi/internal/kube" 10 | "rusi/pkg/injector" 11 | ) 12 | 13 | func main() { 14 | //https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md 15 | klog.InitFlags(nil) 16 | kube.InitFlags(nil) 17 | injector.BindConfigFlags(nil) 18 | 19 | flag.Parse() 20 | defer klog.Flush() 21 | 22 | cfg, err := injector.GetConfig() 23 | if err != nil { 24 | klog.Fatalf("error getting config: %s", err) 25 | } 26 | ctx := context.Background() 27 | 28 | kubeClient := kube.GetKubeClient() 29 | var authUIDs []string 30 | 31 | if cfg.ValidateServiceAccount { 32 | authUIDs, err = injector.AllowedControllersServiceAccountUID(ctx, kubeClient) 33 | if err != nil { 34 | log.Fatalf("failed to get authentication uids from services accounts: %s", err) 35 | } 36 | } 37 | err = injector.NewInjector(authUIDs, cfg, kubeClient).Run(ctx) 38 | if err != http.ErrServerClosed { 39 | klog.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cmd/rusid/components.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "k8s.io/klog/v2" 6 | "rusi/pkg/custom-resource/components/middleware" 7 | "rusi/pkg/custom-resource/components/pubsub" 8 | "rusi/pkg/messaging" 9 | "rusi/pkg/messaging/jetstream" 10 | natsstreaming "rusi/pkg/messaging/nats" 11 | "rusi/pkg/runtime" 12 | ) 13 | 14 | func RegisterComponentFactories() (result []runtime.Option) { 15 | result = append(result, 16 | 17 | runtime.WithPubSubs( 18 | pubsub.New("natsstreaming", func() messaging.PubSub { 19 | return natsstreaming.NewNATSStreamingPubSub() 20 | }), 21 | pubsub.New("jetstream", func() messaging.PubSub { 22 | return jetstream.NewJetStreamPubSub() 23 | }), 24 | ), 25 | runtime.WithPubsubMiddleware( 26 | middleware.New("uppercase", func(properties map[string]string) messaging.Middleware { 27 | return func(next messaging.Handler) messaging.Handler { 28 | return func(ctx context.Context, msg *messaging.MessageEnvelope) error { 29 | klog.V(4).InfoS("uppercase middleware hit") 30 | return next(ctx, msg) 31 | } 32 | } 33 | }), 34 | ), 35 | ) 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /pkg/messaging/serdes/serdes.go: -------------------------------------------------------------------------------- 1 | package serdes 2 | 3 | import ( 4 | "fmt" 5 | jsoniter "github.com/json-iterator/go" 6 | "rusi/pkg/messaging" 7 | ) 8 | 9 | func Marshal(data interface{}) ([]byte, error) { 10 | return jsoniter.Marshal(data) 11 | } 12 | 13 | func Unmarshal(data []byte, v interface{}) error { 14 | return jsoniter.Unmarshal(data, v) 15 | } 16 | 17 | func MarshalMessageEnvelope(data *messaging.MessageEnvelope) ([]byte, error) { 18 | return Marshal(data) 19 | } 20 | 21 | func UnmarshalMessageEnvelope(data []byte) (messaging.MessageEnvelope, error) { 22 | env := internalMessageEnvelope{} 23 | env.MessageEnvelope.Headers = map[string]string{} 24 | 25 | if err := Unmarshal(data, &env); err != nil { 26 | return env.MessageEnvelope, err 27 | } 28 | 29 | for key, val := range env.Headers { 30 | if val != nil { 31 | env.MessageEnvelope.Headers[key] = fmt.Sprintf("%v", val) 32 | } else { 33 | env.MessageEnvelope.Headers[key] = "" 34 | } 35 | } 36 | return env.MessageEnvelope, nil 37 | } 38 | 39 | type internalMessageEnvelope struct { 40 | messaging.MessageEnvelope 41 | Headers map[string]interface{} `json:"Headers"` 42 | } 43 | -------------------------------------------------------------------------------- /pkg/custom-resource/components/types.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type ComponentCategory string 8 | type Operation string 9 | 10 | const ( 11 | BindingsComponent ComponentCategory = "bindings" 12 | PubsubComponent ComponentCategory = "pubsub" 13 | SecretStoreComponent ComponentCategory = "secretstores" 14 | MiddlewareComponent ComponentCategory = "middleware" 15 | 16 | Insert Operation = "insert" 17 | Update Operation = "update" 18 | Delete Operation = "delete" 19 | 20 | DefaultComponentInitTimeout = time.Second * 5 21 | DefaultGracefulShutdownDuration = time.Second * 5 22 | ) 23 | 24 | var ComponentCategories = []ComponentCategory{ 25 | BindingsComponent, 26 | PubsubComponent, 27 | SecretStoreComponent, 28 | MiddlewareComponent, 29 | } 30 | 31 | type Spec struct { 32 | Name string 33 | Type string 34 | Version string `json:"version" yaml:"version"` 35 | Metadata map[string]string `json:"metadata" yaml:"metadata"` 36 | Scopes []string `json:"scopes" yaml:"scopes"` 37 | } 38 | 39 | type ChangeNotification struct { 40 | ComponentCategory ComponentCategory 41 | Operation Operation 42 | ComponentSpec Spec 43 | } 44 | -------------------------------------------------------------------------------- /internal/metrics/prometheus.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "go.opentelemetry.io/otel" 6 | "go.opentelemetry.io/otel/exporters/prometheus" 7 | "go.opentelemetry.io/otel/sdk/metric" 8 | "go.opentelemetry.io/otel/sdk/resource" 9 | semconv "go.opentelemetry.io/otel/semconv/v1.17.0" 10 | "k8s.io/klog/v2" 11 | ) 12 | 13 | func SetupPrometheusMetrics(appId string) *prometheus.Exporter { 14 | exporter, err := prometheus.New(prometheus.WithoutScopeInfo()) 15 | if err != nil { 16 | klog.ErrorS(err, "failed to initialize prometheus exporter") 17 | return nil 18 | } 19 | 20 | r, _ := resource.New(context.Background(), 21 | resource.WithHost(), 22 | resource.WithAttributes(semconv.ServiceName(appId))) 23 | 24 | r, _ = resource.Merge(resource.Default(), r) 25 | 26 | provider := metric.NewMeterProvider(metric.WithResource(r), 27 | metric.WithReader(exporter), 28 | //metric.WithView(metric.NewView( 29 | // metric.Instrument{ 30 | // Name: "rusi.pubsub.processing.duration", 31 | // }, 32 | // metric.Stream{ 33 | // Aggregation: metric.AggregationExplicitBucketHistogram{ 34 | // Boundaries: []float64{10, 100, 1000, 5000, 10000, 20000, 100000}, 35 | // }, 36 | // })), 37 | ) 38 | 39 | otel.SetMeterProvider(provider) 40 | 41 | return exporter 42 | } 43 | -------------------------------------------------------------------------------- /pkg/operator/apis/rusi/v1alpha1/register.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | 8 | "rusi/pkg/operator/apis/rusi" 9 | ) 10 | 11 | // SchemeGroupVersion is group version used to register these objects. 12 | var SchemeGroupVersion = schema.GroupVersion{Group: rusi.GroupName, Version: "v1alpha1"} 13 | 14 | // Kind takes an unqualified kind and returns back a Group qualified GroupKind. 15 | func Kind(kind string) schema.GroupKind { 16 | return SchemeGroupVersion.WithKind(kind).GroupKind() 17 | } 18 | 19 | // Resource takes an unqualified resource and returns a Group qualified GroupResource. 20 | func Resource(resource string) schema.GroupResource { 21 | return SchemeGroupVersion.WithResource(resource).GroupResource() 22 | } 23 | 24 | var ( 25 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 26 | AddToScheme = SchemeBuilder.AddToScheme 27 | ) 28 | 29 | // Adds the list of known types to Scheme. 30 | func addKnownTypes(scheme *runtime.Scheme) error { 31 | scheme.AddKnownTypes( 32 | SchemeGroupVersion, 33 | &Component{}, 34 | &ComponentList{}, 35 | &Configuration{}, 36 | &ConfigurationList{}, 37 | ) 38 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/custom-resource/configuration/loader/local.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "os" 7 | "rusi/pkg/custom-resource/configuration" 8 | 9 | yaml "gopkg.in/yaml.v2" 10 | ) 11 | 12 | // LoadDefaultConfiguration returns the default config. 13 | func LoadDefaultConfiguration() configuration.Spec { 14 | return configuration.Spec{ 15 | Telemetry: configuration.TelemetrySpec{}, 16 | PubSubSpec: configuration.PubSubSpec{ 17 | Name: "", 18 | }, 19 | } 20 | } 21 | 22 | // LoadStandaloneConfiguration gets the path to a config file and loads it into a configuration. 23 | func LoadStandaloneConfiguration(config string) func(ctx context.Context) (<-chan configuration.Spec, error) { 24 | return func(ctx context.Context) (<-chan configuration.Spec, error) { 25 | spec := LoadDefaultConfiguration() 26 | c := make(chan configuration.Spec) 27 | 28 | _, err := os.Stat(config) 29 | if err != nil { 30 | return c, err 31 | } 32 | 33 | b, err := ioutil.ReadFile(config) 34 | if err != nil { 35 | return c, err 36 | } 37 | 38 | // Parse environment variables from yaml 39 | b = []byte(os.ExpandEnv(string(b))) 40 | 41 | cfg := configuration.Configuration{Spec: spec} 42 | err = yaml.Unmarshal(b, &cfg) 43 | 44 | go func() { 45 | c <- cfg.Spec 46 | }() 47 | 48 | return c, err 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pkg/runtime/service/subscriber.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "rusi/pkg/messaging" 6 | "rusi/pkg/middleware" 7 | 8 | "k8s.io/klog/v2" 9 | ) 10 | 11 | type subscriberService struct { 12 | subscriber messaging.Subscriber 13 | pipeline messaging.Pipeline 14 | } 15 | 16 | func NewSubscriberService(subscriber messaging.Subscriber, pipeline messaging.Pipeline) *subscriberService { 17 | return &subscriberService{subscriber, pipeline} 18 | } 19 | 20 | func (srv *subscriberService) StartSubscribing(request messaging.SubscribeRequest) (messaging.CloseFunc, error) { 21 | 22 | //insert tracing/metrics by default 23 | srv.pipeline.UseMiddleware(middleware.SubscriberMetricsMiddleware()) 24 | srv.pipeline.UseMiddleware(middleware.SubscriberTracingMiddleware()) 25 | pipe := srv.pipeline.Build(request.Handler) 26 | 27 | klog.V(4).InfoS("subscribed to", "topic", request.Topic, "options", request.Options) 28 | 29 | return srv.subscriber.Subscribe(request.Topic, func(ctx context.Context, env *messaging.MessageEnvelope) error { 30 | klog.V(4).InfoS("message received on", "topic", request.Topic, "message", env) 31 | 32 | ctx = context.WithValue(ctx, messaging.TopicKey, request.Topic) 33 | err := pipe(ctx, env) 34 | if err != nil { 35 | klog.ErrorS(err, "error calling handler") 36 | } 37 | return err 38 | }, request.Options) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/operator/client/listers/rusi/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | // ComponentListerExpansion allows custom methods to be added to 22 | // ComponentLister. 23 | type ComponentListerExpansion interface{} 24 | 25 | // ComponentNamespaceListerExpansion allows custom methods to be added to 26 | // ComponentNamespaceLister. 27 | type ComponentNamespaceListerExpansion interface{} 28 | 29 | // ConfigurationListerExpansion allows custom methods to be added to 30 | // ConfigurationLister. 31 | type ConfigurationListerExpansion interface{} 32 | 33 | // ConfigurationNamespaceListerExpansion allows custom methods to be added to 34 | // ConfigurationNamespaceLister. 35 | type ConfigurationNamespaceListerExpansion interface{} 36 | -------------------------------------------------------------------------------- /internal/diagnostics/diagnostics.go: -------------------------------------------------------------------------------- 1 | package diagnostics 2 | 3 | import ( 4 | "context" 5 | "rusi/pkg/custom-resource/configuration" 6 | configuration_loader "rusi/pkg/custom-resource/configuration/loader" 7 | 8 | "k8s.io/klog/v2" 9 | ) 10 | 11 | func WatchConfig(ctx context.Context, configLoader configuration_loader.ConfigurationLoader, 12 | tracerFunc func(url string, propagator configuration.TelemetryPropagator) (func(), error)) { 13 | 14 | var ( 15 | prevConf configuration.Spec 16 | tracingStopper func() 17 | ) 18 | 19 | configChan, err := configLoader(ctx) 20 | if err != nil { 21 | klog.ErrorS(err, "error loading application config") 22 | } 23 | 24 | for cfg := range configChan { 25 | changed := cfg.Telemetry.CollectorEndpoint != prevConf.Telemetry.CollectorEndpoint || cfg.Telemetry.Tracing != prevConf.Telemetry.Tracing 26 | validConfig := cfg.Telemetry.CollectorEndpoint != "" && cfg.Telemetry.Tracing.Propagator != "" 27 | if changed { 28 | if tracingStopper != nil { 29 | //flush prev logs 30 | tracingStopper() 31 | } 32 | if validConfig { 33 | tracingStopper, err = tracerFunc(cfg.Telemetry.CollectorEndpoint, cfg.Telemetry.Tracing.Propagator) 34 | if err != nil { 35 | klog.ErrorS(err, "error creating tracer") 36 | } 37 | } 38 | } 39 | 40 | prevConf = cfg 41 | } 42 | 43 | if tracingStopper != nil { 44 | //flush logs 45 | tracingStopper() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1/fake/fake_rusi_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | v1alpha1 "rusi/pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1" 23 | 24 | rest "k8s.io/client-go/rest" 25 | testing "k8s.io/client-go/testing" 26 | ) 27 | 28 | type FakeRusiV1alpha1 struct { 29 | *testing.Fake 30 | } 31 | 32 | func (c *FakeRusiV1alpha1) Components(namespace string) v1alpha1.ComponentInterface { 33 | return &FakeComponents{c, namespace} 34 | } 35 | 36 | func (c *FakeRusiV1alpha1) Configurations(namespace string) v1alpha1.ConfigurationInterface { 37 | return &FakeConfigurations{c, namespace} 38 | } 39 | 40 | // RESTClient returns a RESTClient that is used to communicate 41 | // with API server by this client implementation. 42 | func (c *FakeRusiV1alpha1) RESTClient() rest.Interface { 43 | var ret *rest.RESTClient 44 | return ret 45 | } 46 | -------------------------------------------------------------------------------- /pkg/operator/client/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package internalinterfaces 20 | 21 | import ( 22 | versioned "rusi/pkg/operator/client/clientset/versioned" 23 | time "time" 24 | 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | cache "k8s.io/client-go/tools/cache" 28 | ) 29 | 30 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 31 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 32 | 33 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 34 | type SharedInformerFactory interface { 35 | Start(stopCh <-chan struct{}) 36 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 37 | } 38 | 39 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 40 | type TweakListOptionsFunc func(*v1.ListOptions) 41 | -------------------------------------------------------------------------------- /pkg/injector/config.go: -------------------------------------------------------------------------------- 1 | package injector 2 | 3 | import ( 4 | "flag" 5 | "github.com/kelseyhightower/envconfig" 6 | ) 7 | 8 | var defaultConfig = NewConfigWithDefaults() 9 | 10 | // Config represents configuration options for the Rusi Sidecar Injector webhook server. 11 | type Config struct { 12 | TLSCertFile string `envconfig:"TLS_CERT_FILE" required:"true"` 13 | TLSKeyFile string `envconfig:"TLS_KEY_FILE" required:"true"` 14 | SidecarImage string `envconfig:"SIDECAR_IMAGE" required:"true"` 15 | SidecarImagePullPolicy string `envconfig:"SIDECAR_IMAGE_PULL_POLICY"` 16 | Namespace string `envconfig:"NAMESPACE" required:"true"` 17 | KubeClusterDomain string `envconfig:"KUBE_CLUSTER_DOMAIN"` 18 | ValidateServiceAccount bool 19 | } 20 | 21 | // NewConfigWithDefaults returns a Config object with default values already 22 | // applied. Callers are then free to set custom values for the remaining fields 23 | // and/or override default values. 24 | func NewConfigWithDefaults() Config { 25 | return Config{ 26 | SidecarImagePullPolicy: "Always", 27 | ValidateServiceAccount: true, 28 | } 29 | } 30 | 31 | func BindConfigFlags(flagset *flag.FlagSet) { 32 | if flagset == nil { 33 | flagset = flag.CommandLine 34 | } 35 | 36 | flagset.BoolVar(&defaultConfig.ValidateServiceAccount, "validate_service_account", defaultConfig.ValidateServiceAccount, "If true, injector will validate that rusi sidecars can only be created by a specified list of serviceAccounts") 37 | } 38 | 39 | // GetConfig returns configuration derived from environment variables. 40 | func GetConfig() (Config, error) { 41 | err := envconfig.Process("", &defaultConfig) 42 | return defaultConfig, err 43 | } 44 | -------------------------------------------------------------------------------- /helm/charts/rusi_sidecar_injector/templates/ClusterRoleBinding.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: rusi-operator 5 | subjects: 6 | - kind: ServiceAccount 7 | name: rusi-operator 8 | namespace: {{ .Release.Namespace }} 9 | roleRef: 10 | kind: ClusterRole 11 | name: rusi-operator-admin 12 | apiGroup: rbac.authorization.k8s.io 13 | --- 14 | kind: ClusterRoleBinding 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | metadata: 17 | name: rusi-role-tokenreview-binding 18 | subjects: 19 | - kind: ServiceAccount 20 | name: rusi-operator 21 | namespace: {{ .Release.Namespace }} 22 | roleRef: 23 | apiGroup: rbac.authorization.k8s.io 24 | kind: ClusterRole 25 | name: system:auth-delegator 26 | --- 27 | kind: ClusterRole 28 | apiVersion: rbac.authorization.k8s.io/v1 29 | metadata: 30 | name: rusi-operator-admin 31 | rules: 32 | - apiGroups: ["*"] 33 | resources: ["serviceaccounts", "deployments", "services", "configmaps", "secrets", "components", "configurations", "leases"] 34 | verbs: ["get"] 35 | - apiGroups: ["*"] 36 | resources: ["deployments", "services", "components", "configurations", "subscriptions", "leases"] 37 | verbs: ["list"] 38 | - apiGroups: ["*"] 39 | resources: ["deployments", "services", "components", "configurations", "subscriptions", "leases"] 40 | verbs: ["watch"] 41 | - apiGroups: ["*"] 42 | resources: ["services", "secrets", "configmaps", "leases", "services/finalizers", "deployments/finalizers"] 43 | verbs: ["update"] 44 | - apiGroups: ["*"] 45 | resources: ["services", "leases"] 46 | verbs: ["delete"] 47 | - apiGroups: ["*"] 48 | resources: ["deployments", "services", "configmaps", "events", "leases"] 49 | verbs: ["create"] 50 | -------------------------------------------------------------------------------- /pkg/operator/client/informers/externalversions/rusi/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package rusi 20 | 21 | import ( 22 | internalinterfaces "rusi/pkg/operator/client/informers/externalversions/internalinterfaces" 23 | v1alpha1 "rusi/pkg/operator/client/informers/externalversions/rusi/v1alpha1" 24 | ) 25 | 26 | // Interface provides access to each of this group's versions. 27 | type Interface interface { 28 | // V1alpha1 provides access to shared informers for resources in V1alpha1. 29 | V1alpha1() v1alpha1.Interface 30 | } 31 | 32 | type group struct { 33 | factory internalinterfaces.SharedInformerFactory 34 | namespace string 35 | tweakListOptions internalinterfaces.TweakListOptionsFunc 36 | } 37 | 38 | // New returns a new Interface. 39 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 40 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 41 | } 42 | 43 | // V1alpha1 returns a new v1alpha1.Interface. 44 | func (g *group) V1alpha1() v1alpha1.Interface { 45 | return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/messaging/types.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | const ( 9 | TopicKey = "topic" 10 | ) 11 | 12 | //MessageEnvelope should be cloudevent compatible 13 | //spec https://github.com/cloudevents/spec/blob/v1.0.1/spec.md#type 14 | type MessageEnvelope struct { 15 | Id string `json:"id"` 16 | Type string `json:"type"` 17 | SpecVersion string `json:"specversion"` 18 | DataContentType string `json:"datacontenttype"` 19 | Time time.Time `json:"time"` 20 | Subject string `json:"subject"` 21 | //data is not used yet 22 | //Data interface{} `json:"data"` 23 | 24 | //For backward compatibility 25 | Headers map[string]string `json:"Headers"` // hack for conductor 26 | Payload interface{} `json:"Payload"` // hack for conductor 27 | } 28 | 29 | type CloseFunc func() error 30 | type AckHandler func(string, error) 31 | 32 | type Handler func(ctx context.Context, msg *MessageEnvelope) error 33 | type HandlerWithAck func(ctx context.Context, msg *MessageEnvelope, ackHandler AckHandler) error 34 | 35 | type PublishRequest struct { 36 | PubsubName string 37 | Topic string 38 | Data interface{} 39 | Type string 40 | DataContentType string 41 | Metadata map[string]string 42 | } 43 | 44 | type SubscribeRequest struct { 45 | PubsubName string 46 | Topic string 47 | Handler Handler 48 | Options *SubscriptionOptions 49 | } 50 | 51 | type SubscriptionOptions struct { 52 | Durable *bool 53 | QGroup *bool 54 | MaxConcurrentMessages *int32 55 | DeliverNewMessagesOnly *bool 56 | AckWaitTime *time.Duration 57 | } 58 | 59 | type PublishRequestHandler func(ctx context.Context, request PublishRequest) error 60 | type SubscribeRequestHandler func(ctx context.Context, request SubscribeRequest) (CloseFunc, error) 61 | -------------------------------------------------------------------------------- /pkg/operator/apis/rusi/v1alpha1/componentTypes.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | // +genclient 9 | // +genclient:noStatus 10 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 11 | 12 | // Component describes a Rusi component type. 13 | type Component struct { 14 | metav1.TypeMeta `json:",inline"` 15 | // +optional 16 | metav1.ObjectMeta `json:"metadata,omitempty"` 17 | // +optional 18 | Spec ComponentSpec `json:"spec,omitempty"` 19 | // +optional 20 | Auth `json:"auth,omitempty"` 21 | // +optional 22 | Scopes []string `json:"scopes,omitempty"` 23 | } 24 | 25 | // ComponentSpec is the spec for a component. 26 | type ComponentSpec struct { 27 | Type string `json:"type"` 28 | Version string `json:"version"` 29 | // +optional 30 | IgnoreErrors bool `json:"ignoreErrors"` 31 | Metadata []MetadataItem `json:"metadata"` 32 | // +optional 33 | InitTimeout string `json:"initTimeout"` 34 | } 35 | 36 | // MetadataItem is a name/value pair for a metadata. 37 | type MetadataItem struct { 38 | Name string `json:"name"` 39 | // +optional 40 | Value apiextensionsv1.JSON `json:"value,omitempty"` 41 | // +optional 42 | SecretKeyRef SecretKeyRef `json:"secretKeyRef,omitempty"` 43 | } 44 | 45 | // SecretKeyRef is a reference to a secret holding the value for the metadata item. Name is the secret name, and key is the field in the secret. 46 | type SecretKeyRef struct { 47 | Name string `json:"name"` 48 | Key string `json:"key"` 49 | } 50 | 51 | // Auth represents authentication details for the component. 52 | type Auth struct { 53 | SecretStore string `json:"secretStore"` 54 | } 55 | 56 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 57 | 58 | // ComponentList is a list of Rusi components. 59 | type ComponentList struct { 60 | metav1.TypeMeta `json:",inline"` 61 | metav1.ListMeta `json:"metadata"` 62 | 63 | Items []Component `json:"items"` 64 | } 65 | -------------------------------------------------------------------------------- /helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "k8s_operator.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "k8s_operator.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "k8s_operator.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "k8s_operator.labels" -}} 37 | helm.sh/chart: {{ include "k8s_operator.chart" . }} 38 | {{ include "k8s_operator.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "k8s_operator.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "k8s_operator.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "k8s_operator.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "k8s_operator.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | rusiv1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 23 | 24 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | schema "k8s.io/apimachinery/pkg/runtime/schema" 27 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | ) 30 | 31 | var scheme = runtime.NewScheme() 32 | var codecs = serializer.NewCodecFactory(scheme) 33 | 34 | var localSchemeBuilder = runtime.SchemeBuilder{ 35 | rusiv1alpha1.AddToScheme, 36 | } 37 | 38 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 39 | // of clientsets, like in: 40 | // 41 | // import ( 42 | // "k8s.io/client-go/kubernetes" 43 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 44 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 45 | // ) 46 | // 47 | // kclientset, _ := kubernetes.NewForConfig(c) 48 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 49 | // 50 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 51 | // correctly. 52 | var AddToScheme = localSchemeBuilder.AddToScheme 53 | 54 | func init() { 55 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 56 | utilruntime.Must(AddToScheme(scheme)) 57 | } 58 | -------------------------------------------------------------------------------- /helm/charts/rusi_sidecar_injector/templates/sidecar_injector_webhook_config.yaml: -------------------------------------------------------------------------------- 1 | {{- $existingSecret := lookup "v1" "Secret" .Release.Namespace "rusi-sidecar-injector-cert"}} 2 | {{- $existingWebHookConfig := lookup "admissionregistration.k8s.io/v1" "MutatingWebhookConfiguration" .Release.Namespace "rusi-sidecar-injector"}} 3 | {{- $ca := genCA "rusi-sidecar-injector-ca" 3650 }} 4 | {{- $cn := printf "rusi-sidecar-injector" }} 5 | {{- $altName1 := printf "rusi-sidecar-injector.%s" .Release.Namespace }} 6 | {{- $altName2 := printf "rusi-sidecar-injector.%s.svc" .Release.Namespace }} 7 | {{- $altName3 := printf "rusi-sidecar-injector.%s.svc.cluster" .Release.Namespace }} 8 | {{- $altName4 := printf "rusi-sidecar-injector.%s.svc.cluster.local" .Release.Namespace }} 9 | {{- $cert := genSignedCert $cn nil (list $altName1 $altName2 $altName3 $altName4) 3650 $ca }} 10 | apiVersion: v1 11 | kind: Secret 12 | metadata: 13 | name: rusi-sidecar-injector-cert 14 | labels: 15 | app: rusi-sidecar-injector 16 | data: 17 | {{ if $existingSecret }}tls.crt: {{ index $existingSecret.data "tls.crt" }} 18 | {{ else }}tls.crt: {{ b64enc $cert.Cert }} 19 | {{ end }} 20 | 21 | {{ if $existingSecret }}tls.key: {{ index $existingSecret.data "tls.key" }} 22 | {{ else }}tls.key: {{ b64enc $cert.Key }} 23 | {{ end }} 24 | --- 25 | apiVersion: admissionregistration.k8s.io/v1 26 | kind: MutatingWebhookConfiguration 27 | metadata: 28 | name: rusi-sidecar-injector 29 | labels: 30 | app: rusi-sidecar-injector 31 | webhooks: 32 | - name: sidecar-injector.rusi.io 33 | clientConfig: 34 | service: 35 | namespace: {{ .Release.Namespace }} 36 | name: rusi-sidecar-injector 37 | path: "/mutate" 38 | caBundle: {{ if $existingWebHookConfig }}{{ (index $existingWebHookConfig.webhooks 0).clientConfig.caBundle }}{{ else }}{{ b64enc $ca.Cert }}{{ end }} 39 | rules: 40 | - apiGroups: 41 | - "" 42 | apiVersions: 43 | - v1 44 | resources: 45 | - pods 46 | operations: 47 | - CREATE 48 | failurePolicy: {{ .Values.webhookFailurePolicy}} 49 | sideEffects: None 50 | admissionReviewVersions: ["v1", "v1beta1"] -------------------------------------------------------------------------------- /pkg/operator/client/informers/externalversions/rusi/v1alpha1/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | internalinterfaces "rusi/pkg/operator/client/informers/externalversions/internalinterfaces" 23 | ) 24 | 25 | // Interface provides access to all the informers in this group version. 26 | type Interface interface { 27 | // Components returns a ComponentInformer. 28 | Components() ComponentInformer 29 | // Configurations returns a ConfigurationInformer. 30 | Configurations() ConfigurationInformer 31 | } 32 | 33 | type version struct { 34 | factory internalinterfaces.SharedInformerFactory 35 | namespace string 36 | tweakListOptions internalinterfaces.TweakListOptionsFunc 37 | } 38 | 39 | // New returns a new Interface. 40 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 41 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 42 | } 43 | 44 | // Components returns a ComponentInformer. 45 | func (v *version) Components() ComponentInformer { 46 | return &componentInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 47 | } 48 | 49 | // Configurations returns a ConfigurationInformer. 50 | func (v *version) Configurations() ConfigurationInformer { 51 | return &configurationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 52 | } 53 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package scheme 20 | 21 | import ( 22 | rusiv1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 23 | 24 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | schema "k8s.io/apimachinery/pkg/runtime/schema" 27 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | ) 30 | 31 | var Scheme = runtime.NewScheme() 32 | var Codecs = serializer.NewCodecFactory(Scheme) 33 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 34 | var localSchemeBuilder = runtime.SchemeBuilder{ 35 | rusiv1alpha1.AddToScheme, 36 | } 37 | 38 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 39 | // of clientsets, like in: 40 | // 41 | // import ( 42 | // "k8s.io/client-go/kubernetes" 43 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 44 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 45 | // ) 46 | // 47 | // kclientset, _ := kubernetes.NewForConfig(c) 48 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 49 | // 50 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 51 | // correctly. 52 | var AddToScheme = localSchemeBuilder.AddToScheme 53 | 54 | func init() { 55 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 56 | utilruntime.Must(AddToScheme(Scheme)) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/custom-resource/components/pubsub/registry.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "rusi/pkg/custom-resource/components" 6 | "rusi/pkg/messaging" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // PubSubDefinition is a pub/sub component definition. 12 | PubSubDefinition struct { 13 | Name string 14 | FactoryMethod func() messaging.PubSub 15 | } 16 | 17 | // Registry is the interface for callers to get registered pub-sub components. 18 | Registry interface { 19 | Register(components ...PubSubDefinition) 20 | Create(name, version string) (messaging.PubSub, error) 21 | } 22 | 23 | pubSubRegistry struct { 24 | messageBuses map[string]func() messaging.PubSub 25 | } 26 | ) 27 | 28 | // New creates a PubSub. 29 | func New(name string, factoryMethod func() messaging.PubSub) PubSubDefinition { 30 | return PubSubDefinition{ 31 | Name: name, 32 | FactoryMethod: factoryMethod, 33 | } 34 | } 35 | 36 | // NewRegistry returns a new pub sub registry. 37 | func NewRegistry() Registry { 38 | return &pubSubRegistry{ 39 | messageBuses: map[string]func() messaging.PubSub{}, 40 | } 41 | } 42 | 43 | // Register registers one or more new message buses. 44 | func (p *pubSubRegistry) Register(components ...PubSubDefinition) { 45 | for _, component := range components { 46 | p.messageBuses[createFullName(component.Name)] = component.FactoryMethod 47 | } 48 | } 49 | 50 | // Create instantiates a pub/sub based on `name`. 51 | func (p *pubSubRegistry) Create(name, version string) (messaging.PubSub, error) { 52 | if method, ok := p.getPubSub(name, version); ok { 53 | return method(), nil 54 | } 55 | return nil, errors.Errorf("couldn't find message bus %s/%s", name, version) 56 | } 57 | 58 | func (p *pubSubRegistry) getPubSub(name, version string) (func() messaging.PubSub, bool) { 59 | nameLower := strings.ToLower(name) 60 | versionLower := strings.ToLower(version) 61 | pubSubFn, ok := p.messageBuses[nameLower+"/"+versionLower] 62 | if ok { 63 | return pubSubFn, true 64 | } 65 | if components.IsInitialVersion(versionLower) { 66 | pubSubFn, ok = p.messageBuses[nameLower] 67 | } 68 | return pubSubFn, ok 69 | } 70 | 71 | func createFullName(name string) string { 72 | return strings.ToLower("pubsub." + name) 73 | } 74 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Variables # 3 | ################################################################################ 4 | 5 | OUT_DIR := ./dist 6 | BINARIES ?= rusid injector operator 7 | GIT_COMMIT = $(shell git rev-list -1 HEAD) 8 | GIT_VERSION = $(shell git describe --always --abbrev=7 --dirty) 9 | RUSI_VERSION ?= edge 10 | 11 | # Helm template and install setting 12 | HELM:=helm 13 | RELEASE_NAME?=rusi 14 | RUSI_NAMESPACE?=rusi-system 15 | HELM_CHART_ROOT:=./helm 16 | 17 | ################################################################################ 18 | # Go build details # 19 | ################################################################################ 20 | BASE_PACKAGE_NAME := rusi 21 | 22 | DEFAULT_LDFLAGS:=-X $(BASE_PACKAGE_NAME)/internal/version.gitcommit=$(GIT_COMMIT) \ 23 | -X $(BASE_PACKAGE_NAME)/internal/version.gitversion=$(GIT_VERSION) \ 24 | -X $(BASE_PACKAGE_NAME)/internal/version.version=$(RUSI_VERSION) 25 | 26 | ################################################################################ 27 | # Target: build-linux # 28 | ################################################################################ 29 | build-linux: 30 | mkdir -p $(OUT_DIR) 31 | CGO_ENABLED=0 GOOS=linux go build -o $(OUT_DIR) -ldflags "$(DEFAULT_LDFLAGS) -s -w" ./cmd/rusid ./cmd/injector ./cmd/operator 32 | 33 | modtidy: 34 | go mod tidy 35 | 36 | init-proto: 37 | go get google.golang.org/protobuf/cmd/protoc-gen-go google.golang.org/grpc/cmd/protoc-gen-go-grpc 38 | 39 | clean-proto: 40 | rm -rf pkg/proto/* 41 | 42 | gen-proto: 43 | protoc proto/runtime/v1/* --go-grpc_out=. --go_out=. --go-grpc_opt=require_unimplemented_servers=false 44 | protoc proto/operator/v1/* --go-grpc_out=. --go_out=. --go-grpc_opt=require_unimplemented_servers=false 45 | 46 | upgrade-all: 47 | go get -u ./... 48 | go mod tidy 49 | 50 | test: 51 | go test -race `go list ./... | grep -v 'rusi/pkg/operator'` 52 | 53 | testV: 54 | go test -race -v `go list ./... | grep -v 'rusi/pkg/operator'` 55 | 56 | include docker/docker.mk 57 | include pkg/operator/tools/generate_kube_crd.mk 58 | -------------------------------------------------------------------------------- /proto/runtime/v1/rusi.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package rusi.proto.runtime.v1; 4 | 5 | // import "google/protobuf/any.proto"; 6 | import "google/protobuf/empty.proto"; 7 | import "google/protobuf/duration.proto"; 8 | import "google/protobuf/wrappers.proto"; 9 | 10 | option go_package = "pkg/proto/runtime/v1"; 11 | option csharp_namespace = "Proto.V1"; 12 | 13 | service Rusi { 14 | 15 | // Publishes events to the specific topic. 16 | rpc Publish(PublishRequest) returns (google.protobuf.Empty); 17 | 18 | // Subscribe pushes events on the stream 19 | rpc Subscribe(stream SubscribeRequest) returns (stream ReceivedMessage); 20 | } 21 | 22 | // PublishRequest is the message to publish data to pubsub topic 23 | message PublishRequest { 24 | // The name of the pubsub component 25 | string pubsub_name = 1; 26 | 27 | // The pubsub topic 28 | string topic = 2; 29 | 30 | // The data which will be published to topic. 31 | bytes data = 3; 32 | 33 | // This attribute contains a value describing the type of event related to the originating occurrence. 34 | string type = 4; 35 | 36 | // The content type for the data (optional). 37 | string data_content_type = 5; 38 | 39 | // The metadata passing to pub components 40 | // 41 | // metadata property: 42 | // - key : the key of the message. 43 | map metadata = 6; 44 | } 45 | 46 | message SubscribeRequest { 47 | oneof RequestType { 48 | SubscriptionRequest subscription_request = 1; 49 | AckRequest ack_request = 2; 50 | } 51 | } 52 | 53 | message AckRequest { 54 | string message_id = 1; 55 | string error = 2; 56 | } 57 | 58 | message SubscriptionRequest { 59 | // The name of the pubsub component 60 | string pubsub_name = 1; 61 | 62 | // The pubsub topic 63 | string topic = 2; 64 | 65 | SubscriptionOptions options = 3; 66 | } 67 | 68 | message SubscriptionOptions { 69 | google.protobuf.BoolValue durable = 1; 70 | google.protobuf.BoolValue qGroup = 2; 71 | google.protobuf.Int32Value maxConcurrentMessages = 3; 72 | google.protobuf.BoolValue deliverNewMessagesOnly = 4; 73 | google.protobuf.Duration ackWaitTime = 5; 74 | } 75 | 76 | 77 | message ReceivedMessage { 78 | string id = 1; 79 | // The data which will be published to topic. 80 | bytes data = 2; 81 | map metadata = 3; 82 | } 83 | -------------------------------------------------------------------------------- /pkg/injector/validation.go: -------------------------------------------------------------------------------- 1 | package injector 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "regexp" 7 | ) 8 | 9 | // The consts and vars beginning with dns* were taken from: https://github.com/kubernetes/apimachinery/blob/fc49b38c19f02a58ebc476347e622142f19820b9/pkg/util/validation/validation.go 10 | const ( 11 | dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" 12 | dns1123LabelErrMsg string = "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character" 13 | dns1123LabelMaxLength int = 63 14 | ) 15 | 16 | var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$") 17 | 18 | // ValidateKubernetesAppID returns a bool that indicates whether a rusi app id is valid for the Kubernetes platform. 19 | func ValidateKubernetesAppID(appID string) error { 20 | if appID == "" { 21 | return errors.New("value for the rusi.io/app-id annotation is empty") 22 | } 23 | return nil 24 | } 25 | 26 | // The function was taken as-is from: https://github.com/kubernetes/apimachinery/blob/fc49b38c19f02a58ebc476347e622142f19820b9/pkg/util/validation/validation.go 27 | func isDNS1123Label(value string) []string { 28 | var errs []string 29 | if len(value) > dns1123LabelMaxLength { 30 | errs = append(errs, maxLenError(dns1123LabelMaxLength)) 31 | } 32 | if !dns1123LabelRegexp.MatchString(value) { 33 | errs = append(errs, regexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc")) 34 | } 35 | return errs 36 | } 37 | 38 | // The function was taken as-is from: https://github.com/kubernetes/apimachinery/blob/fc49b38c19f02a58ebc476347e622142f19820b9/pkg/util/validation/validation.go 39 | func maxLenError(length int) string { 40 | return fmt.Sprintf("must be no more than %d characters", length) 41 | } 42 | 43 | // The function was taken as-is from: https://github.com/kubernetes/apimachinery/blob/fc49b38c19f02a58ebc476347e622142f19820b9/pkg/util/validation/validation.go 44 | func regexError(msg string, fmt string, examples ...string) string { 45 | if len(examples) == 0 { 46 | return msg + " (regex used for validation is '" + fmt + "')" 47 | } 48 | msg += " (e.g. " 49 | for i := range examples { 50 | if i > 0 { 51 | msg += " or " 52 | } 53 | msg += "'" + examples[i] + "', " 54 | } 55 | msg += "regex used for validation is '" + fmt + "')" 56 | return msg 57 | } 58 | -------------------------------------------------------------------------------- /pkg/custom-resource/configuration/types.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | const ( 4 | AllowAccess = "allow" 5 | DenyAccess = "deny" 6 | DefaultTrustDomain = "public" 7 | ) 8 | 9 | type Feature string 10 | 11 | type Configuration struct { 12 | Spec Spec `json:"spec" yaml:"spec"` 13 | } 14 | 15 | type Spec struct { 16 | SubscriberPipelineSpec PipelineSpec `json:"subscriberPipeline,omitempty" yaml:"subscriberPipeline,omitempty"` 17 | PublisherPipelineSpec PipelineSpec `json:"publisherPipeline,omitempty" yaml:"publisherPipeline,omitempty"` 18 | Telemetry TelemetrySpec `json:"telemetry,omitempty" yaml:"telemetry,omitempty"` 19 | Features []FeatureSpec `json:"features,omitempty" yaml:"features,omitempty"` 20 | PubSubSpec PubSubSpec `json:"pubSub,omitempty" yaml:"pubSub,omitempty"` 21 | MinRuntimeVersion string `json:"minRuntimeVersion,omitempty" yaml:"minRuntimeVersion,omitempty"` 22 | } 23 | 24 | // PipelineSpec defines the middleware pipeline. 25 | type PipelineSpec struct { 26 | Handlers []HandlerSpec `json:"handlers" yaml:"handlers"` 27 | } 28 | 29 | // HandlerSpec defines a request handlers. 30 | type HandlerSpec struct { 31 | Name string `json:"name" yaml:"name"` 32 | Type string `json:"type" yaml:"type"` 33 | Version string `json:"version" yaml:"version"` 34 | } 35 | 36 | // Telemetry related configuration. 37 | type TelemetrySpec struct { 38 | // Telemetry collector enpoint address. 39 | CollectorEndpoint string `json:"collectorEndpoint" yaml:"collectorEndpoint"` 40 | // Tracing configuration. 41 | // +optional 42 | Tracing TracingSpec `json:"tracing,omitempty" yaml:"tracing"` 43 | } 44 | 45 | type TelemetryPropagator string 46 | 47 | const ( 48 | TelemetryPropagatorW3c = TelemetryPropagator("w3c") 49 | TelemetryPropagatorJaeger = TelemetryPropagator("jaeger") 50 | ) 51 | 52 | // TracingSpec defines distributed tracing configuration. 53 | type TracingSpec struct { 54 | // Telemetry propagator. Possible values: w3c, jaeger 55 | Propagator TelemetryPropagator `json:"propagator" yaml:"propagator"` 56 | } 57 | 58 | // FeatureSpec defines the features that are enabled/disabled. 59 | type FeatureSpec struct { 60 | Name Feature `json:"name" yaml:"name"` 61 | Enabled bool `json:"enabled" yaml:"enabled"` 62 | } 63 | 64 | type PubSubSpec struct { 65 | Name string `json:"name" yaml:"name"` 66 | } 67 | -------------------------------------------------------------------------------- /internal/kube/client.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "k8s.io/client-go/kubernetes" 7 | "k8s.io/client-go/rest" 8 | "k8s.io/client-go/tools/clientcmd" 9 | "k8s.io/client-go/util/homedir" 10 | "k8s.io/klog/v2" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | var ( 17 | clientSet *kubernetes.Clientset 18 | kubeConfig *rest.Config 19 | kubeConfigPath string 20 | ) 21 | 22 | func initKubeConfig() { 23 | kubeConfig = GetConfig() 24 | clientset, err := kubernetes.NewForConfig(kubeConfig) 25 | if err != nil { 26 | klog.Fatal(err) 27 | } 28 | 29 | clientSet = clientset 30 | } 31 | 32 | // GetConfig gets a kubernetes rest config. 33 | func GetConfig() *rest.Config { 34 | if kubeConfig != nil { 35 | return kubeConfig 36 | } 37 | 38 | conf, err := rest.InClusterConfig() 39 | if err != nil { 40 | conf, err = clientcmd.BuildConfigFromFlags("", kubeConfigPath) 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | return conf 47 | } 48 | 49 | // InitFlags is for explicitly initializing the flags. 50 | func InitFlags(flagset *flag.FlagSet) { 51 | if flagset == nil { 52 | flagset = flag.CommandLine 53 | } 54 | 55 | if home := homedir.HomeDir(); home != "" { 56 | flag.StringVar(&kubeConfigPath, "kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") 57 | } else { 58 | flag.StringVar(&kubeConfigPath, "kubeconfig", "", "absolute path to the kubeconfig file") 59 | } 60 | } 61 | 62 | // GetKubeClient gets a kubernetes client. 63 | func GetKubeClient() *kubernetes.Clientset { 64 | if clientSet == nil { 65 | initKubeConfig() 66 | } 67 | 68 | return clientSet 69 | } 70 | 71 | func GetCurrentNamespace() string { 72 | // This way assumes you've set the POD_NAMESPACE environment variable using the downward API. 73 | // This check has to be done first for backwards compatibility with the way InClusterConfig was originally set up 74 | if ns := os.Getenv("POD_NAMESPACE"); ns != "" { 75 | return ns 76 | } 77 | 78 | if ns := os.Getenv("NAMESPACE"); ns != "" { 79 | return ns 80 | } 81 | 82 | // Fall back to the namespace associated with the service account token, if available 83 | if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { 84 | if ns := strings.TrimSpace(string(data)); len(ns) > 0 { 85 | return ns 86 | } 87 | } 88 | 89 | return "default" 90 | } 91 | -------------------------------------------------------------------------------- /pkg/operator/client/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package externalversions 20 | 21 | import ( 22 | "fmt" 23 | v1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 24 | 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | cache "k8s.io/client-go/tools/cache" 27 | ) 28 | 29 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 30 | // sharedInformers based on type 31 | type GenericInformer interface { 32 | Informer() cache.SharedIndexInformer 33 | Lister() cache.GenericLister 34 | } 35 | 36 | type genericInformer struct { 37 | informer cache.SharedIndexInformer 38 | resource schema.GroupResource 39 | } 40 | 41 | // Informer returns the SharedIndexInformer. 42 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 43 | return f.informer 44 | } 45 | 46 | // Lister returns the GenericLister. 47 | func (f *genericInformer) Lister() cache.GenericLister { 48 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 49 | } 50 | 51 | // ForResource gives generic access to a shared informer of the matching type 52 | // TODO extend this to unknown resources with a client pool 53 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 54 | switch resource { 55 | // Group=rusi.io, Version=v1alpha1 56 | case v1alpha1.SchemeGroupVersion.WithResource("components"): 57 | return &genericInformer{resource: resource.GroupResource(), informer: f.Rusi().V1alpha1().Components().Informer()}, nil 58 | case v1alpha1.SchemeGroupVersion.WithResource("configurations"): 59 | return &genericInformer{resource: resource.GroupResource(), informer: f.Rusi().V1alpha1().Configurations().Informer()}, nil 60 | 61 | } 62 | 63 | return nil, fmt.Errorf("no informer found for %v", resource) 64 | } 65 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "go.opentelemetry.io/otel" 6 | "go.opentelemetry.io/otel/attribute" 7 | "go.opentelemetry.io/otel/metric" 8 | "go.opentelemetry.io/otel/metric/noop" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type serviceMetrics struct { 14 | pubsubMeter metric.Meter 15 | publishCount metric.Int64Counter 16 | subscribeDurationMs metric.Int64Histogram 17 | subscribeDurationSec metric.Float64Histogram 18 | } 19 | 20 | var ( 21 | initOnce sync.Once 22 | s *serviceMetrics 23 | ) 24 | 25 | func DefaultPubSubMetrics() *serviceMetrics { 26 | initOnce.Do(func() { 27 | s = newServiceMetrics() 28 | }) 29 | return s 30 | } 31 | 32 | func newServiceMetrics() *serviceMetrics { 33 | pubsubM := otel.GetMeterProvider().Meter("rusi.io/pubsub") 34 | 35 | publishCount, _ := pubsubM.Int64Counter("rusi.pubsub.publish.count", 36 | metric.WithDescription("The number of publishes")) 37 | 38 | //TODO should be removed 39 | subscribeDurationMs, _ := pubsubM.Int64Histogram("rusi.pubsub.processing.duration", 40 | metric.WithDescription("The duration of a message execution"), 41 | metric.WithUnit("milliseconds")) 42 | 43 | subscribeDurationSec, _ := pubsubM.Float64Histogram("rusi.pubsub.processing.duration.seconds", 44 | metric.WithDescription("The duration of a message execution"), 45 | metric.WithUnit("seconds"), 46 | metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 4, 5, 7.5, 10, 15, 20, 30, 35, 40)) 47 | 48 | return &serviceMetrics{ 49 | pubsubMeter: pubsubM, 50 | publishCount: publishCount, 51 | subscribeDurationMs: subscribeDurationMs, 52 | subscribeDurationSec: subscribeDurationSec, 53 | } 54 | } 55 | 56 | func (s *serviceMetrics) RecordPublishMessage(ctx context.Context, topic string, success bool) { 57 | opt := metric.WithAttributes( 58 | attribute.String("topic", topic), 59 | attribute.Bool("success", success), 60 | ) 61 | s.publishCount.Add(ctx, 1, opt) 62 | } 63 | 64 | func (s *serviceMetrics) RecordSubscriberProcessingTime(ctx context.Context, topic string, success bool, elapsed time.Duration) { 65 | opt := metric.WithAttributes( 66 | attribute.String("topic", topic), 67 | attribute.Bool("success", success), 68 | ) 69 | s.subscribeDurationMs.Record(ctx, elapsed.Milliseconds(), opt) 70 | s.subscribeDurationSec.Record(ctx, elapsed.Seconds(), opt) 71 | } 72 | 73 | func SetNoopMeterProvider() { 74 | otel.SetMeterProvider(noop.NewMeterProvider()) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/custom-resource/components/middleware/registry.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "rusi/pkg/custom-resource/components" 6 | "rusi/pkg/messaging" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // Middleware is a Pubsub middleware component definition. 12 | Middleware struct { 13 | Name string 14 | FactoryMethod func(properties map[string]string) messaging.Middleware 15 | } 16 | 17 | // Registry is the interface for callers to get registered Pubsub middleware. 18 | Registry interface { 19 | Register(components ...Middleware) 20 | Create(name, version string, properties map[string]string) (messaging.Middleware, error) 21 | } 22 | 23 | pubsubMiddlewareRegistry struct { 24 | middleware map[string]func(properties map[string]string) messaging.Middleware 25 | } 26 | ) 27 | 28 | // New creates a Middleware. 29 | func New(name string, factoryMethod func(properties map[string]string) messaging.Middleware) Middleware { 30 | return Middleware{ 31 | Name: name, 32 | FactoryMethod: factoryMethod, 33 | } 34 | } 35 | 36 | // NewRegistry returns a new Pubsub middleware registry. 37 | func NewRegistry() Registry { 38 | return &pubsubMiddlewareRegistry{ 39 | middleware: map[string]func(properties map[string]string) messaging.Middleware{}, 40 | } 41 | } 42 | 43 | // Register registers one or more new Pubsub middlewares. 44 | func (p *pubsubMiddlewareRegistry) Register(components ...Middleware) { 45 | for _, component := range components { 46 | p.middleware[createFullName(component.Name)] = component.FactoryMethod 47 | } 48 | } 49 | 50 | // Create instantiates a Pubsub middleware based on `name`. 51 | func (p *pubsubMiddlewareRegistry) Create(name, version string, properties map[string]string) (messaging.Middleware, error) { 52 | if method, ok := p.getMiddleware(name, version); ok { 53 | return method(properties), nil 54 | } 55 | return nil, errors.Errorf("Pubsub middleware %s/%s has not been registered", name, version) 56 | } 57 | 58 | func (p *pubsubMiddlewareRegistry) getMiddleware(name, version string) (func(properties map[string]string) messaging.Middleware, bool) { 59 | nameLower := strings.ToLower(name) 60 | versionLower := strings.ToLower(version) 61 | middlewareFn, ok := p.middleware[nameLower+"/"+versionLower] 62 | if ok { 63 | return middlewareFn, true 64 | } 65 | if components.IsInitialVersion(versionLower) { 66 | middlewareFn, ok = p.middleware[nameLower] 67 | } 68 | return middlewareFn, ok 69 | } 70 | 71 | func createFullName(name string) string { 72 | return strings.ToLower("middleware.pubsub." + name) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/operator/server.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "rusi/pkg/custom-resource/components" 5 | rusiv1 "rusi/pkg/operator/apis/rusi/v1alpha1" 6 | operatorv1 "rusi/pkg/proto/operator/v1" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/google/uuid" 11 | jsoniter "github.com/json-iterator/go" 12 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 13 | "k8s.io/klog/v2" 14 | ) 15 | 16 | type operatorServer struct { 17 | ow *objectWatcher 18 | } 19 | 20 | func (opsrv *operatorServer) WatchConfiguration(request *operatorv1.WatchConfigurationRequest, stream operatorv1.RusiOperator_WatchConfigurationServer) error { 21 | c := make(chan rusiv1.Configuration) 22 | opsrv.ow.addConfigurationListener(c) 23 | defer opsrv.ow.removeConfigurationListener(c) 24 | 25 | for { 26 | select { 27 | case obj := <-c: 28 | if obj.Namespace == request.Namespace && obj.Name == request.ConfigName { 29 | b, _ := jsoniter.Marshal(obj.Spec) 30 | stream.Send(&operatorv1.GenericItem{ 31 | Data: b, 32 | }) 33 | } 34 | case <-stream.Context().Done(): 35 | klog.V(4).ErrorS(stream.Context().Err(), "grpc WatchConfiguration stream closed") 36 | return nil 37 | } 38 | } 39 | } 40 | 41 | func (opsrv *operatorServer) WatchComponents(request *operatorv1.WatchComponentsRequest, stream operatorv1.RusiOperator_WatchComponentsServer) error { 42 | c := make(chan rusiv1.Component) 43 | opsrv.ow.addComponentListener(c) 44 | defer opsrv.ow.removeComponentListener(c) 45 | 46 | for { 47 | select { 48 | case obj := <-c: 49 | if obj.Namespace == request.Namespace { 50 | b, _ := jsoniter.Marshal(convertToComponent(obj)) 51 | stream.Send(&operatorv1.GenericItem{ 52 | Data: b, 53 | }) 54 | } 55 | case <-stream.Context().Done(): 56 | klog.V(4).ErrorS(stream.Context().Err(), "grpc WatchComponents stream closed") 57 | return nil 58 | } 59 | } 60 | } 61 | 62 | func convertToComponent(item rusiv1.Component) components.Spec { 63 | return components.Spec{ 64 | Name: item.Name, 65 | Type: item.Spec.Type, 66 | Version: item.Spec.Version, 67 | Metadata: convertMetadataItemsToProperties(item.Spec.Metadata), 68 | Scopes: item.Scopes, 69 | } 70 | } 71 | 72 | func convertMetadataItemsToProperties(items []rusiv1.MetadataItem) map[string]string { 73 | properties := map[string]string{} 74 | for _, c := range items { 75 | val := JsonToUnquotedString(c.Value) 76 | for strings.Contains(val, "{uuid}") { 77 | val = strings.Replace(val, "{uuid}", uuid.New().String(), 1) 78 | } 79 | properties[c.Name] = val 80 | } 81 | return properties 82 | } 83 | 84 | func JsonToUnquotedString(json apiextensionsv1.JSON) string { 85 | s := string(json.Raw) 86 | c, err := strconv.Unquote(s) 87 | if err == nil { 88 | s = c 89 | } 90 | return s 91 | } 92 | -------------------------------------------------------------------------------- /internal/tracing/tracing.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | configuration "rusi/pkg/custom-resource/configuration" 8 | "strings" 9 | "time" 10 | 11 | jaeger_propagator "go.opentelemetry.io/contrib/propagators/jaeger" 12 | "go.opentelemetry.io/otel" 13 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 14 | "go.opentelemetry.io/otel/propagation" 15 | "go.opentelemetry.io/otel/sdk/resource" 16 | tracesdk "go.opentelemetry.io/otel/sdk/trace" 17 | semconv "go.opentelemetry.io/otel/semconv/v1.17.0" 18 | "k8s.io/klog/v2" 19 | ) 20 | 21 | func SetTracing(serviceName string) func(url string, propagator configuration.TelemetryPropagator) (func(), error) { 22 | return func(url string, propagator configuration.TelemetryPropagator) (func(), error) { 23 | tp, err := getTracerProvider(url, serviceName) 24 | if err != nil { 25 | return nil, err 26 | } 27 | // Register our TracerProvider as the global so any imported 28 | // instrumentation in the future will default to using it. 29 | otel.SetTracerProvider(tp) 30 | if propagator == configuration.TelemetryPropagatorJaeger { 31 | otel.SetTextMapPropagator(jaeger_propagator.Jaeger{}) 32 | } else { 33 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) 34 | } 35 | 36 | return func() { 37 | flushTracer(tp) 38 | }, nil 39 | } 40 | } 41 | 42 | // FlushTracer cleanly shutdown and flush telemetry when the application exits. 43 | func flushTracer(tp *tracesdk.TracerProvider) func() { 44 | return func() { 45 | 46 | klog.V(4).InfoS("Trying to stop TracerProvider") 47 | 48 | // Do not make the application hang when it is shutdown. 49 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*6) 50 | defer cancel() 51 | if err := tp.Shutdown(ctx); err != nil { 52 | klog.ErrorS(err, "Tracer shutdown error") 53 | } 54 | } 55 | } 56 | 57 | func getTracerProvider(url string, serviceName string) (*tracesdk.TracerProvider, error) { 58 | // Set up a trace exporter 59 | url = strings.TrimPrefix(url, "http://") 60 | 61 | traceExporter, err := otlptracegrpc.New(context.Background(), 62 | otlptracegrpc.WithEndpoint(url), 63 | otlptracegrpc.WithInsecure()) 64 | 65 | if err != nil { 66 | return nil, fmt.Errorf("failed to create trace exporter: %w", err) 67 | } 68 | hostName, _ := os.Hostname() 69 | 70 | bsp := tracesdk.NewBatchSpanProcessor(traceExporter) 71 | tp := tracesdk.NewTracerProvider( 72 | tracesdk.WithSampler(tracesdk.AlwaysSample()), 73 | tracesdk.WithResource(resource.NewWithAttributes( 74 | semconv.SchemaURL, 75 | semconv.ServiceNameKey.String(serviceName), 76 | semconv.HostNameKey.String(hostName), 77 | )), 78 | tracesdk.WithSpanProcessor(bsp), 79 | ) 80 | 81 | return tp, nil 82 | } 83 | -------------------------------------------------------------------------------- /pkg/messaging/inmemory.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "context" 5 | "golang.org/x/sync/errgroup" 6 | "k8s.io/klog/v2" 7 | "sync" 8 | "sync/atomic" 9 | "time" 10 | ) 11 | 12 | type inMemoryBus struct { 13 | handlers map[string][]*Handler 14 | mu sync.RWMutex 15 | workingCounter *int32 16 | } 17 | 18 | func NewInMemoryBus() *inMemoryBus { 19 | return &inMemoryBus{handlers: map[string][]*Handler{}, workingCounter: new(int32)} 20 | } 21 | 22 | func (c *inMemoryBus) Publish(topic string, env *MessageEnvelope) error { 23 | c.mu.RLock() 24 | h := c.handlers[topic] 25 | c.mu.RUnlock() 26 | if env.Headers == nil { 27 | env.Headers = map[string]string{} 28 | } 29 | env.Headers["topic"] = topic 30 | klog.InfoS("Publish to topic " + topic) 31 | 32 | go c.runHandlers(h, env) 33 | return nil 34 | } 35 | 36 | func (c *inMemoryBus) Subscribe(topic string, handler Handler, options *SubscriptionOptions) (CloseFunc, error) { 37 | c.mu.Lock() 38 | defer c.mu.Unlock() 39 | 40 | handlerP := &handler 41 | c.handlers[topic] = append(c.handlers[topic], handlerP) 42 | klog.InfoS("Subscribed to topic " + topic) 43 | 44 | return func() error { 45 | c.mu.Lock() 46 | defer c.mu.Unlock() 47 | var s []*Handler 48 | for _, h := range c.handlers[topic] { 49 | if h != handlerP { 50 | s = append(s, h) 51 | } 52 | } 53 | c.handlers[topic] = s 54 | 55 | klog.InfoS("unSubscribe from topic " + topic) 56 | return nil 57 | }, nil 58 | } 59 | 60 | func (*inMemoryBus) Init(properties map[string]string) error { 61 | return nil 62 | } 63 | 64 | func (*inMemoryBus) Close() error { 65 | return nil 66 | } 67 | 68 | func (c *inMemoryBus) IsDoneWorking() bool { 69 | return atomic.LoadInt32(c.workingCounter) == 0 70 | } 71 | func (c *inMemoryBus) GetSubscribersCount(topic string) int { 72 | c.mu.RLock() 73 | defer c.mu.RUnlock() 74 | return len(c.handlers[topic]) 75 | } 76 | 77 | func (c *inMemoryBus) runHandlers(handlers []*Handler, env *MessageEnvelope) error { 78 | atomic.AddInt32(c.workingCounter, 1) 79 | defer atomic.AddInt32(c.workingCounter, -1) 80 | 81 | klog.InfoS("start runHandlers for topic " + env.Subject) 82 | eg := errgroup.Group{} 83 | c.mu.RLock() 84 | for i, h := range handlers { 85 | h := h 86 | i := i 87 | klog.Infof("starting Handler %d with metadata %v", i, env.Headers) 88 | eg.Go(func() error { 89 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 90 | defer cancel() 91 | return (*h)(ctx, env) //run handler blocks 92 | }) 93 | } 94 | c.mu.RUnlock() 95 | err := eg.Wait() 96 | if err != nil { 97 | klog.ErrorS(err, "error running handlers") 98 | } else { 99 | klog.InfoS("finish runHandlers for topic " + env.Subject) 100 | } 101 | return err 102 | } 103 | -------------------------------------------------------------------------------- /pkg/operator/apis/rusi/v1alpha1/configurationTypes.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +genclient:noStatus 9 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 10 | 11 | // Configuration describes an Rusi configuration setting. 12 | type Configuration struct { 13 | metav1.TypeMeta `json:",inline"` 14 | // +optional 15 | metav1.ObjectMeta `json:"metadata,omitempty"` 16 | // +optional 17 | Spec ConfigurationSpec `json:"spec,omitempty"` 18 | } 19 | 20 | // ConfigurationSpec is the spec for an configuration. 21 | type ConfigurationSpec struct { 22 | // +optional 23 | SubscriberPipelineSpec PipelineSpec `json:"subscriberPipeline,omitempty"` 24 | // +optional 25 | PublisherPipelineSpec PipelineSpec `json:"publisherPipeline,omitempty"` 26 | // +optional 27 | Telemetry TelemetrySpec `json:"telemetry,omitempty"` 28 | // +optional 29 | Features []FeatureSpec `json:"features,omitempty"` 30 | // +optional 31 | PubSubSpec PubSubSpec `json:"pubSub,omitempty"` 32 | // +optional 33 | MinRuntimeVersion string `json:"minRuntimeVersion,omitempty"` 34 | } 35 | 36 | // PipelineSpec defines the middleware pipeline. 37 | type PipelineSpec struct { 38 | Handlers []HandlerSpec `json:"handlers"` 39 | } 40 | 41 | // HandlerSpec defines a request handlers. 42 | type HandlerSpec struct { 43 | Name string `json:"name"` 44 | Type string `json:"type"` 45 | } 46 | 47 | // Telemetry related configuration. 48 | type TelemetrySpec struct { 49 | // Telemetry collector enpoint address. 50 | CollectorEndpoint string `json:"collectorEndpoint"` 51 | // Tracing configuration. 52 | // +optional 53 | Tracing TracingSpec `json:"tracing,omitempty"` 54 | } 55 | 56 | type TelemetryPropagator string 57 | 58 | const ( 59 | TelemetryPropagatorW3c = TelemetryPropagator("w3c") 60 | TelemetryPropagatorJaeger = TelemetryPropagator("jaeger") 61 | ) 62 | 63 | // TracingSpec defines distributed tracing configuration. 64 | type TracingSpec struct { 65 | // Telemetry propagator. Possible values: w3c, jaeger 66 | // +kubebuilder:validation:Enum=w3c;jaeger 67 | // +kubebuilder:default:=w3c 68 | Propagator TelemetryPropagator `json:"propagator"` 69 | } 70 | 71 | // FeatureSpec defines the features that are enabled/disabled. 72 | type FeatureSpec struct { 73 | Name string `json:"name" yaml:"name"` 74 | Enabled bool `json:"enabled" yaml:"enabled"` 75 | } 76 | 77 | // PubSubSpec defines default pubSub configuration. 78 | type PubSubSpec struct { 79 | Name string `json:"name" yaml:"name"` 80 | } 81 | 82 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 83 | 84 | // ConfigurationList is a list of Rusi event sources. 85 | type ConfigurationList struct { 86 | metav1.TypeMeta `json:",inline"` 87 | metav1.ListMeta `json:"metadata"` 88 | 89 | Items []Configuration `json:"items"` 90 | } 91 | -------------------------------------------------------------------------------- /helm/charts/rusi_operator/templates/rusi_operator_deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: rusi-operator 5 | labels: 6 | app: rusi-operator 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: rusi-operator 12 | template: 13 | metadata: 14 | labels: 15 | app: rusi-operator 16 | app.kubernetes.io/name: {{ .Release.Name }} 17 | app.kubernetes.io/version: {{ .Values.global.tag }} 18 | app.kubernetes.io/component: operator 19 | app.kubernetes.io/part-of: "rusi" 20 | app.kubernetes.io/managed-by: "helm" 21 | {{- if eq .Values.global.prometheus.enabled true }} 22 | annotations: 23 | prometheus.io/scrape: "{{ .Values.global.prometheus.enabled }}" 24 | prometheus.io/port: "{{ .Values.global.prometheus.port }}" 25 | prometheus.io/path: "/" 26 | {{- end }} 27 | spec: 28 | containers: 29 | - name: rusi-operator 30 | {{- if contains "/" .Values.image.name }} 31 | image: "{{ .Values.image.name }}" 32 | {{- else }} 33 | image: "{{ .Values.global.registry }}/rusi:{{ .Values.global.tag }}" 34 | {{- end }} 35 | imagePullPolicy: {{ .Values.global.imagePullPolicy }} 36 | securityContext: 37 | runAsNonRoot: {{ .Values.runAsNonRoot }} 38 | {{- if eq .Values.debug.enabled true }} 39 | capabilities: 40 | add: 41 | - SYS_PTRACE 42 | {{- end }} 43 | env: 44 | - name: NAMESPACE 45 | valueFrom: 46 | fieldRef: 47 | fieldPath: metadata.namespace 48 | ports: 49 | - containerPort: 6500 50 | {{- if eq .Values.global.prometheus.enabled true }} 51 | - name: metrics 52 | containerPort: {{ .Values.global.prometheus.port }} 53 | protocol: TCP 54 | {{- end }} 55 | {{- if eq .Values.debug.enabled true }} 56 | - name: debug 57 | containerPort: {{ .Values.debug.port }} 58 | protocol: TCP 59 | {{- end }} 60 | resources: 61 | {{ toYaml .Values.resources | indent 10 }} 62 | command: 63 | {{- if eq .Values.debug.enabled false }} 64 | - "/operator" 65 | {{- else }} 66 | - "/dlv" 67 | {{- end }} 68 | args: 69 | {{- if eq .Values.debug.enabled true }} 70 | - "--listen=:{{ .Values.debug.port }}" 71 | - "--accept-multiclient" 72 | - "--headless=true" 73 | - "--log" 74 | - "--api-version=2" 75 | - "exec" 76 | - "/operator" 77 | - "--" 78 | {{- end }} 79 | - "--v" 80 | - "{{ .Values.logLevel }}" 81 | {{- if eq .Values.global.prometheus.enabled true }} 82 | - "--enable-metrics" 83 | - "--metrics-port" 84 | - "{{ .Values.global.prometheus.port }}" 85 | {{- else }} 86 | #- "--enable-metrics=false" 87 | {{- end }} 88 | serviceAccountName: rusi-operator 89 | {{- if .Values.global.imagePullSecrets }} 90 | imagePullSecrets: 91 | - name: {{ .Values.global.imagePullSecrets }} 92 | {{- end }} -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1/rusi_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | v1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 23 | "rusi/pkg/operator/client/clientset/versioned/scheme" 24 | 25 | rest "k8s.io/client-go/rest" 26 | ) 27 | 28 | type RusiV1alpha1Interface interface { 29 | RESTClient() rest.Interface 30 | ComponentsGetter 31 | ConfigurationsGetter 32 | } 33 | 34 | // RusiV1alpha1Client is used to interact with features provided by the rusi.io group. 35 | type RusiV1alpha1Client struct { 36 | restClient rest.Interface 37 | } 38 | 39 | func (c *RusiV1alpha1Client) Components(namespace string) ComponentInterface { 40 | return newComponents(c, namespace) 41 | } 42 | 43 | func (c *RusiV1alpha1Client) Configurations(namespace string) ConfigurationInterface { 44 | return newConfigurations(c, namespace) 45 | } 46 | 47 | // NewForConfig creates a new RusiV1alpha1Client for the given config. 48 | func NewForConfig(c *rest.Config) (*RusiV1alpha1Client, error) { 49 | config := *c 50 | if err := setConfigDefaults(&config); err != nil { 51 | return nil, err 52 | } 53 | client, err := rest.RESTClientFor(&config) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return &RusiV1alpha1Client{client}, nil 58 | } 59 | 60 | // NewForConfigOrDie creates a new RusiV1alpha1Client for the given config and 61 | // panics if there is an error in the config. 62 | func NewForConfigOrDie(c *rest.Config) *RusiV1alpha1Client { 63 | client, err := NewForConfig(c) 64 | if err != nil { 65 | panic(err) 66 | } 67 | return client 68 | } 69 | 70 | // New creates a new RusiV1alpha1Client for the given RESTClient. 71 | func New(c rest.Interface) *RusiV1alpha1Client { 72 | return &RusiV1alpha1Client{c} 73 | } 74 | 75 | func setConfigDefaults(config *rest.Config) error { 76 | gv := v1alpha1.SchemeGroupVersion 77 | config.GroupVersion = &gv 78 | config.APIPath = "/apis" 79 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 80 | 81 | if config.UserAgent == "" { 82 | config.UserAgent = rest.DefaultKubernetesUserAgent() 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // RESTClient returns a RESTClient that is used to communicate 89 | // with API server by this client implementation. 90 | func (c *RusiV1alpha1Client) RESTClient() rest.Interface { 91 | if c == nil { 92 | return nil 93 | } 94 | return c.restClient 95 | } 96 | -------------------------------------------------------------------------------- /pkg/healthcheck/health.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type response struct { 12 | Status string `json:"status,omitempty"` 13 | Errors map[string]string `json:"errors,omitempty"` 14 | } 15 | 16 | type health struct { 17 | checkers map[string]HealthChecker 18 | timeout time.Duration 19 | } 20 | 21 | // Handler returns an http.Handler 22 | func Handler(opts ...Option) http.Handler { 23 | h := &health{ 24 | checkers: make(map[string]HealthChecker), 25 | timeout: 30 * time.Second, 26 | } 27 | for _, opt := range opts { 28 | opt(h) 29 | } 30 | return h 31 | } 32 | 33 | // HandlerFunc returns an http.HandlerFunc to mount the API implementation at a specific route 34 | func HandlerFunc(opts ...Option) http.HandlerFunc { 35 | return Handler(opts...).ServeHTTP 36 | } 37 | 38 | // Option adds optional parameter for the HealthcheckHandlerFunc 39 | type Option func(*health) 40 | 41 | // WithChecker adds a status checker that needs to be added as part of healthcheck. i.e database, cache or any external dependency 42 | func WithChecker(name string, s HealthChecker) Option { 43 | return func(h *health) { 44 | h.checkers[name] = &timeoutChecker{s} 45 | } 46 | } 47 | 48 | // WithTimeout configures the global timeout for all individual checkers. 49 | func WithTimeout(timeout time.Duration) Option { 50 | return func(h *health) { 51 | h.timeout = timeout 52 | } 53 | } 54 | 55 | func (h *health) ServeHTTP(w http.ResponseWriter, r *http.Request) { 56 | nCheckers := len(h.checkers) 57 | 58 | code := http.StatusOK 59 | errorMsgs := make(map[string]string, nCheckers) 60 | 61 | ctx, cancel := context.Background(), func() {} 62 | if h.timeout > 0 { 63 | ctx, cancel = context.WithTimeout(ctx, h.timeout) 64 | } 65 | defer cancel() 66 | 67 | var mutex sync.Mutex 68 | var wg sync.WaitGroup 69 | wg.Add(nCheckers) 70 | 71 | for key, checker := range h.checkers { 72 | go func(key string, checker HealthChecker) { 73 | if r := checker.IsHealthy(ctx); r.Status != Healthy { 74 | mutex.Lock() 75 | errorMsgs[key] = r.Description 76 | code = http.StatusServiceUnavailable 77 | mutex.Unlock() 78 | } 79 | wg.Done() 80 | }(key, checker) 81 | } 82 | 83 | wg.Wait() 84 | 85 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 86 | w.WriteHeader(code) 87 | json.NewEncoder(w).Encode(response{ 88 | Status: http.StatusText(code), 89 | Errors: errorMsgs, 90 | }) 91 | } 92 | 93 | type timeoutChecker struct { 94 | checker HealthChecker 95 | } 96 | 97 | func (t *timeoutChecker) IsHealthy(ctx context.Context) HealthResult { 98 | checkerChan := make(chan HealthResult) 99 | go func() { 100 | checkerChan <- t.checker.IsHealthy(ctx) 101 | }() 102 | select { 103 | case r := <-checkerChan: 104 | return r 105 | case <-ctx.Done(): 106 | return HealthResult{ 107 | Status: Unhealthy, 108 | Description: "max check time exceeded", 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | clientset "rusi/pkg/operator/client/clientset/versioned" 23 | rusiv1alpha1 "rusi/pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1" 24 | fakerusiv1alpha1 "rusi/pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1/fake" 25 | 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/watch" 28 | "k8s.io/client-go/discovery" 29 | fakediscovery "k8s.io/client-go/discovery/fake" 30 | "k8s.io/client-go/testing" 31 | ) 32 | 33 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 34 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 35 | // without applying any validations and/or defaults. It shouldn't be considered a replacement 36 | // for a real clientset and is mostly useful in simple unit tests. 37 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 38 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 39 | for _, obj := range objects { 40 | if err := o.Add(obj); err != nil { 41 | panic(err) 42 | } 43 | } 44 | 45 | cs := &Clientset{tracker: o} 46 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 47 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 48 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 49 | gvr := action.GetResource() 50 | ns := action.GetNamespace() 51 | watch, err := o.Watch(gvr, ns) 52 | if err != nil { 53 | return false, nil, err 54 | } 55 | return true, watch, nil 56 | }) 57 | 58 | return cs 59 | } 60 | 61 | // Clientset implements clientset.Interface. Meant to be embedded into a 62 | // struct to get a default implementation. This makes faking out just the method 63 | // you want to test easier. 64 | type Clientset struct { 65 | testing.Fake 66 | discovery *fakediscovery.FakeDiscovery 67 | tracker testing.ObjectTracker 68 | } 69 | 70 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 71 | return c.discovery 72 | } 73 | 74 | func (c *Clientset) Tracker() testing.ObjectTracker { 75 | return c.tracker 76 | } 77 | 78 | var ( 79 | _ clientset.Interface = &Clientset{} 80 | _ testing.FakeClient = &Clientset{} 81 | ) 82 | 83 | // RusiV1alpha1 retrieves the RusiV1alpha1Client 84 | func (c *Clientset) RusiV1alpha1() rusiv1alpha1.RusiV1alpha1Interface { 85 | return &fakerusiv1alpha1.FakeRusiV1alpha1{Fake: &c.Fake} 86 | } 87 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package versioned 20 | 21 | import ( 22 | "fmt" 23 | rusiv1alpha1 "rusi/pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1" 24 | 25 | discovery "k8s.io/client-go/discovery" 26 | rest "k8s.io/client-go/rest" 27 | flowcontrol "k8s.io/client-go/util/flowcontrol" 28 | ) 29 | 30 | type Interface interface { 31 | Discovery() discovery.DiscoveryInterface 32 | RusiV1alpha1() rusiv1alpha1.RusiV1alpha1Interface 33 | } 34 | 35 | // Clientset contains the clients for groups. Each group has exactly one 36 | // version included in a Clientset. 37 | type Clientset struct { 38 | *discovery.DiscoveryClient 39 | rusiV1alpha1 *rusiv1alpha1.RusiV1alpha1Client 40 | } 41 | 42 | // RusiV1alpha1 retrieves the RusiV1alpha1Client 43 | func (c *Clientset) RusiV1alpha1() rusiv1alpha1.RusiV1alpha1Interface { 44 | return c.rusiV1alpha1 45 | } 46 | 47 | // Discovery retrieves the DiscoveryClient 48 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 49 | if c == nil { 50 | return nil 51 | } 52 | return c.DiscoveryClient 53 | } 54 | 55 | // NewForConfig creates a new Clientset for the given config. 56 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 57 | // NewForConfig will generate a rate-limiter in configShallowCopy. 58 | func NewForConfig(c *rest.Config) (*Clientset, error) { 59 | configShallowCopy := *c 60 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 61 | if configShallowCopy.Burst <= 0 { 62 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 63 | } 64 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 65 | } 66 | var cs Clientset 67 | var err error 68 | cs.rusiV1alpha1, err = rusiv1alpha1.NewForConfig(&configShallowCopy) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return &cs, nil 78 | } 79 | 80 | // NewForConfigOrDie creates a new Clientset for the given config and 81 | // panics if there is an error in the config. 82 | func NewForConfigOrDie(c *rest.Config) *Clientset { 83 | var cs Clientset 84 | cs.rusiV1alpha1 = rusiv1alpha1.NewForConfigOrDie(c) 85 | 86 | cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) 87 | return &cs 88 | } 89 | 90 | // New creates a new Clientset for the given RESTClient. 91 | func New(c rest.Interface) *Clientset { 92 | var cs Clientset 93 | cs.rusiV1alpha1 = rusiv1alpha1.New(c) 94 | 95 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 96 | return &cs 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rusi 2 | =============== 3 | >Runtime Sidecar - a [Dapr](https://github.com/dapr/dapr) inspired story 4 | 5 | ![rusi](assets/logo.png) 6 | 7 | ## Contributing on windows 8 | 9 | - Install Go: 10 | - Download Go for windows; Go 1.17 11 | - Editor: 12 | - Visual studio Code + Go extension 13 | - Build: 14 | - install make 15 | ```shell 16 | choco install make 17 | ``` 18 | - install protoc 19 | ```shell 20 | choco install protoc 21 | ``` 22 | - build with make 23 | ```shell 24 | make build-linux 25 | ``` 26 | - Debug: 27 | - install delve debug tool: dlv-dap - suggested by vs-code 28 | - debug with vscode :> Run sidecar 29 | - Test: 30 | - install gcc 31 | ```shell 32 | choco install mingw 33 | ``` 34 | - test with make 35 | ```shell 36 | make test 37 | ``` 38 | 39 | ## Usage 40 | 41 | ### Configure Components 42 | 43 | In self-hosted mode, component files have to be saved on the local machine, and provide a `components-path` to the sidecar. 44 | In Kubernetes mode Rusi will query the kubernetes api in order to find and register all components 45 | 46 | 1. Create a pub/sub message broker component 47 | ```yaml 48 | apiVersion: rusi.io/v1alpha1 49 | kind: Component 50 | metadata: 51 | name: natsstreaming-pubsub 52 | spec: 53 | type: pubsub.natsstreaming 54 | version: v1 55 | metadata: 56 | - name: natsURL 57 | value: "replace with your host" 58 | - name: natsStreamingClusterID 59 | value: "replace with your cluster name" 60 | # below are subscription configuration. 61 | - name: subscriptionType 62 | value: queue # Required. Allowed values: topic, queue. 63 | - name: ackWaitTime 64 | value: "" # Optional. 65 | - name: maxInFlight 66 | value: "1" # Optional. 67 | - name: durableSubscriptionName 68 | value: "" # Optional. 69 | ``` 70 | 71 | 2. Add custom middlewares (optional) 72 | ```yaml 73 | apiVersion: rusi.io/v1alpha1 74 | kind: Component 75 | metadata: 76 | name: uppercase 77 | spec: 78 | type: middleware.http.uppercase 79 | version: v1 80 | ``` 81 | 3. Configure middleware pipeline (optional) 82 | 83 | configuration is not mandatory, unless you want to specify a specific pipeline for pubsub 84 | ```yaml 85 | apiVersion: rusi.io/v1alpha1 86 | kind: Configuration 87 | metadata: 88 | name: node-pipeline-config 89 | spec: 90 | subscriberPipeline: 91 | handlers: 92 | - name: pubsub-uppercase 93 | type: middleware.pubsub.uppercase 94 | # add other middlewares 95 | publisherPipeline: 96 | handlers: 97 | - name: pubsub-uppercase 98 | type: middleware.pubsub.uppercase 99 | # add other middlewares 100 | ``` 101 | ### run rusid 102 | - kubernetes 103 | ```shell 104 | go run cmd/rusid/sidecar.go --mode kubernetes --app-id your-app-id --config "kube-config-resource-name" 105 | ``` 106 | If you run out of cluster, it will use your kubectl current context, to scan for all components in all namespaces. 107 | - standalone 108 | ```shell 109 | go run cmd/rusid/sidecar.go --app-id your-app-id --components-path="path-to-components-folder" --config "path-to-config.yaml" 110 | ``` 111 | -------------------------------------------------------------------------------- /helm/crds/rusi.io_components.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.17.1 7 | name: components.rusi.io 8 | spec: 9 | group: rusi.io 10 | names: 11 | kind: Component 12 | listKind: ComponentList 13 | plural: components 14 | singular: component 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Component describes a Rusi component type. 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | auth: 30 | description: Auth represents authentication details for the component. 31 | properties: 32 | secretStore: 33 | type: string 34 | required: 35 | - secretStore 36 | type: object 37 | kind: 38 | description: |- 39 | Kind is a string value representing the REST resource this object represents. 40 | Servers may infer this from the endpoint the client submits requests to. 41 | Cannot be updated. 42 | In CamelCase. 43 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 44 | type: string 45 | metadata: 46 | type: object 47 | scopes: 48 | items: 49 | type: string 50 | type: array 51 | spec: 52 | description: ComponentSpec is the spec for a component. 53 | properties: 54 | ignoreErrors: 55 | type: boolean 56 | initTimeout: 57 | type: string 58 | metadata: 59 | items: 60 | description: MetadataItem is a name/value pair for a metadata. 61 | properties: 62 | name: 63 | type: string 64 | secretKeyRef: 65 | description: SecretKeyRef is a reference to a secret holding 66 | the value for the metadata item. Name is the secret name, 67 | and key is the field in the secret. 68 | properties: 69 | key: 70 | type: string 71 | name: 72 | type: string 73 | required: 74 | - key 75 | - name 76 | type: object 77 | value: 78 | x-kubernetes-preserve-unknown-fields: true 79 | required: 80 | - name 81 | type: object 82 | type: array 83 | type: 84 | type: string 85 | version: 86 | type: string 87 | required: 88 | - metadata 89 | - type 90 | - version 91 | type: object 92 | type: object 93 | served: true 94 | storage: true 95 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | release: 10 | types: [published] 11 | workflow_dispatch: 12 | 13 | env: 14 | # Use docker.io for Docker Hub if empty 15 | REGISTRY: ghcr.io 16 | # github.repository as / 17 | IMAGE_NAME: ${{ github.repository }} 18 | 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | packages: write 26 | env: 27 | ARTIFACT_DIR: ./release 28 | HELM_PACKAGE_DIR: helm 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v2 32 | 33 | - name: Set up Go 34 | uses: actions/setup-go@v2 35 | with: 36 | go-version: 1.25 37 | 38 | - name: Set release version 39 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/v}" >> $GITHUB_ENV 40 | 41 | - name: Build 42 | env: 43 | RUSI_VERSION: ${{ env.RELEASE_VERSION }} 44 | run: make build-linux 45 | 46 | # Login against a Docker registry except on PR 47 | # https://github.com/docker/login-action 48 | - name: Log into registry ${{ env.REGISTRY }} 49 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 50 | with: 51 | registry: ${{ env.REGISTRY }} 52 | username: ${{ github.actor }} 53 | password: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | - name: Build images 56 | env: 57 | RUSI_REGISTRY: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 58 | RUSI_TAG: ${{ env.RELEASE_VERSION }} 59 | run: make docker-build 60 | 61 | - name: Push images 62 | env: 63 | RUSI_REGISTRY: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 64 | RUSI_TAG: ${{ env.RELEASE_VERSION }} 65 | run: make docker-push 66 | 67 | - name: Package Helm chart 68 | if: ${{ env.LATEST_RELEASE }} == "true" 69 | env: 70 | HELM_CHARTS_DIR: helm 71 | run: | 72 | sed -i "/ tag:/c\ tag: \"${{ env.RELEASE_VERSION }}\"" ${{ env.HELM_CHARTS_DIR }}/values.yaml 73 | mkdir -p ${{ env.ARTIFACT_DIR }}/${{ env.HELM_PACKAGE_DIR }} 74 | helm package ${{ env.HELM_CHARTS_DIR }} --app-version ${{ env.RELEASE_VERSION }} --version ${{ env.RELEASE_VERSION }} --destination ${{ env.ARTIFACT_DIR }}/${{ env.HELM_PACKAGE_DIR }} 75 | 76 | - name: Checkout Helm Charts Repo 77 | uses: actions/checkout@v2 78 | env: 79 | HELM_REPO: osstotalsoft/helm-charts 80 | HELM_REPO_CODE_PATH: helm-charts 81 | with: 82 | repository: ${{ env.HELM_REPO }} 83 | ref: refs/heads/main 84 | token: ${{ secrets.BOT_TOKEN }} 85 | path: ${{ env.HELM_REPO_CODE_PATH }} 86 | 87 | - name: Upload helm charts to Helm Repo 88 | env: 89 | HELM_REPO_CODE_PATH: helm-charts 90 | HELM_REPO: https://osstotalsoft.github.io/helm-charts/ 91 | run: | 92 | cd ${{ env.ARTIFACT_DIR }}/${{ env.HELM_PACKAGE_DIR }} 93 | cp -r * $GITHUB_WORKSPACE/${{ env.HELM_REPO_CODE_PATH }} 94 | cd $GITHUB_WORKSPACE/${{ env.HELM_REPO_CODE_PATH }} 95 | helm repo index --url ${{ env.HELM_REPO }} --merge index.yaml . 96 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 97 | git config --global user.name "github-actions" 98 | git add --all 99 | git commit -m "Rusi release - ${{ env.RELEASE_VERSION }}" 100 | git push 101 | 102 | -------------------------------------------------------------------------------- /pkg/operator/client/informers/externalversions/rusi/v1alpha1/component.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | "context" 23 | rusiv1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 24 | versioned "rusi/pkg/operator/client/clientset/versioned" 25 | internalinterfaces "rusi/pkg/operator/client/informers/externalversions/internalinterfaces" 26 | v1alpha1 "rusi/pkg/operator/client/listers/rusi/v1alpha1" 27 | time "time" 28 | 29 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | runtime "k8s.io/apimachinery/pkg/runtime" 31 | watch "k8s.io/apimachinery/pkg/watch" 32 | cache "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | // ComponentInformer provides access to a shared informer and lister for 36 | // Components. 37 | type ComponentInformer interface { 38 | Informer() cache.SharedIndexInformer 39 | Lister() v1alpha1.ComponentLister 40 | } 41 | 42 | type componentInformer struct { 43 | factory internalinterfaces.SharedInformerFactory 44 | tweakListOptions internalinterfaces.TweakListOptionsFunc 45 | namespace string 46 | } 47 | 48 | // NewComponentInformer constructs a new informer for Component type. 49 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 50 | // one. This reduces memory footprint and number of connections to the server. 51 | func NewComponentInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 52 | return NewFilteredComponentInformer(client, namespace, resyncPeriod, indexers, nil) 53 | } 54 | 55 | // NewFilteredComponentInformer constructs a new informer for Component type. 56 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 57 | // one. This reduces memory footprint and number of connections to the server. 58 | func NewFilteredComponentInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 59 | return cache.NewSharedIndexInformer( 60 | &cache.ListWatch{ 61 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 62 | if tweakListOptions != nil { 63 | tweakListOptions(&options) 64 | } 65 | return client.RusiV1alpha1().Components(namespace).List(context.TODO(), options) 66 | }, 67 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 68 | if tweakListOptions != nil { 69 | tweakListOptions(&options) 70 | } 71 | return client.RusiV1alpha1().Components(namespace).Watch(context.TODO(), options) 72 | }, 73 | }, 74 | &rusiv1alpha1.Component{}, 75 | resyncPeriod, 76 | indexers, 77 | ) 78 | } 79 | 80 | func (f *componentInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 81 | return NewFilteredComponentInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 82 | } 83 | 84 | func (f *componentInformer) Informer() cache.SharedIndexInformer { 85 | return f.factory.InformerFor(&rusiv1alpha1.Component{}, f.defaultInformer) 86 | } 87 | 88 | func (f *componentInformer) Lister() v1alpha1.ComponentLister { 89 | return v1alpha1.NewComponentLister(f.Informer().GetIndexer()) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/runtime/config.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "rusi/pkg/modes" 7 | "rusi/pkg/utils" 8 | "strconv" 9 | ) 10 | 11 | const ( 12 | defaultGRPCHost = "localhost" 13 | defaultGRPCPort = 50003 14 | defaultDiagnosticsPort = 8080 15 | defaultEnableMetrics = true 16 | ) 17 | 18 | type ConfigBuilder struct { 19 | mode string 20 | rusiGRPCPort int 21 | rusiGRPCHost string 22 | diagnosticsPort int 23 | enableMetrics bool 24 | componentsPath string 25 | config string 26 | controlPlaneAddress string 27 | appID string 28 | } 29 | 30 | type Config struct { 31 | Mode modes.RusiMode 32 | RusiGRPCHost string 33 | RusiGRPCPort int 34 | DiagnosticsPort int 35 | EnableMetrics bool 36 | ComponentsPath string 37 | Config string 38 | ControlPlaneAddress string 39 | AppID string 40 | } 41 | 42 | func NewRuntimeConfigBuilder() ConfigBuilder { 43 | return ConfigBuilder{} 44 | } 45 | 46 | func (c *ConfigBuilder) AttachCmdFlags( 47 | stringVar func(p *string, name string, value string, usage string), 48 | boolVar func(p *bool, name string, value bool, usage string), 49 | intVar func(p *int, name string, value int, usage string)) { 50 | 51 | stringVar(&c.mode, "mode", string(modes.StandaloneMode), "Runtime mode for Rusi (kubernetes / standalone - default:standalone )") 52 | stringVar(&c.rusiGRPCHost, "rusi-grpc-host", defaultGRPCHost, "gRPC host for the Rusi API to listen on") 53 | intVar(&c.rusiGRPCPort, "rusi-grpc-port", defaultGRPCPort, "gRPC port for the Rusi API to listen on") 54 | stringVar(&c.componentsPath, "components-path", "", "Path for components directory. If empty, components will not be loaded. Self-hosted mode only") 55 | stringVar(&c.config, "config", "", "Path to config file, or name of a configuration object") 56 | stringVar(&c.controlPlaneAddress, "control-plane-address", "", "Address for Rusi control plane") 57 | stringVar(&c.appID, "app-id", "", "A unique ID for Rusi. Used for Service Discovery and state") 58 | intVar(&c.diagnosticsPort, "diagnostics-port", defaultDiagnosticsPort, "Sets the HTTP port for the diagnostics server (healthz and metrics)") 59 | boolVar(&c.enableMetrics, "enable-metrics", defaultEnableMetrics, "Enable prometheus metrics endpoint") 60 | } 61 | 62 | func (c *ConfigBuilder) Build() (Config, error) { 63 | err := c.validate() 64 | if err != nil { 65 | return Config{}, err 66 | } 67 | 68 | variables := map[string]string{ 69 | utils.AppID: c.appID, 70 | utils.RusiGRPCHost: c.rusiGRPCHost, 71 | utils.RusiGRPCPort: strconv.Itoa(c.rusiGRPCPort), 72 | } 73 | 74 | if err = setEnvVariables(variables); err != nil { 75 | return Config{}, err 76 | } 77 | 78 | return Config{ 79 | Mode: modes.RusiMode(c.mode), 80 | RusiGRPCHost: c.rusiGRPCHost, 81 | RusiGRPCPort: c.rusiGRPCPort, 82 | ComponentsPath: c.componentsPath, 83 | Config: c.config, 84 | AppID: c.appID, 85 | DiagnosticsPort: c.diagnosticsPort, 86 | EnableMetrics: c.enableMetrics, 87 | ControlPlaneAddress: c.controlPlaneAddress, 88 | }, nil 89 | } 90 | 91 | func (c *ConfigBuilder) validate() error { 92 | if c.appID == "" { 93 | return errors.New("app-id parameter cannot be empty") 94 | } 95 | if c.config == "" { 96 | return errors.New("config parameter cannot be empty") 97 | } 98 | 99 | if modes.RusiMode(c.mode) == modes.KubernetesMode && c.controlPlaneAddress == "" { 100 | return errors.New("controlPlaneAddress is mandatory in kubernetes mode") 101 | } 102 | return nil 103 | } 104 | 105 | func setEnvVariables(variables map[string]string) error { 106 | for key, value := range variables { 107 | err := os.Setenv(key, value) 108 | if err != nil { 109 | return err 110 | } 111 | } 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /pkg/operator/client/listers/rusi/v1alpha1/component.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | v1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 23 | 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | "k8s.io/apimachinery/pkg/labels" 26 | "k8s.io/client-go/tools/cache" 27 | ) 28 | 29 | // ComponentLister helps list Components. 30 | // All objects returned here must be treated as read-only. 31 | type ComponentLister interface { 32 | // List lists all Components in the indexer. 33 | // Objects returned here must be treated as read-only. 34 | List(selector labels.Selector) (ret []*v1alpha1.Component, err error) 35 | // Components returns an object that can list and get Components. 36 | Components(namespace string) ComponentNamespaceLister 37 | ComponentListerExpansion 38 | } 39 | 40 | // componentLister implements the ComponentLister interface. 41 | type componentLister struct { 42 | indexer cache.Indexer 43 | } 44 | 45 | // NewComponentLister returns a new ComponentLister. 46 | func NewComponentLister(indexer cache.Indexer) ComponentLister { 47 | return &componentLister{indexer: indexer} 48 | } 49 | 50 | // List lists all Components in the indexer. 51 | func (s *componentLister) List(selector labels.Selector) (ret []*v1alpha1.Component, err error) { 52 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 53 | ret = append(ret, m.(*v1alpha1.Component)) 54 | }) 55 | return ret, err 56 | } 57 | 58 | // Components returns an object that can list and get Components. 59 | func (s *componentLister) Components(namespace string) ComponentNamespaceLister { 60 | return componentNamespaceLister{indexer: s.indexer, namespace: namespace} 61 | } 62 | 63 | // ComponentNamespaceLister helps list and get Components. 64 | // All objects returned here must be treated as read-only. 65 | type ComponentNamespaceLister interface { 66 | // List lists all Components in the indexer for a given namespace. 67 | // Objects returned here must be treated as read-only. 68 | List(selector labels.Selector) (ret []*v1alpha1.Component, err error) 69 | // Get retrieves the Component from the indexer for a given namespace and name. 70 | // Objects returned here must be treated as read-only. 71 | Get(name string) (*v1alpha1.Component, error) 72 | ComponentNamespaceListerExpansion 73 | } 74 | 75 | // componentNamespaceLister implements the ComponentNamespaceLister 76 | // interface. 77 | type componentNamespaceLister struct { 78 | indexer cache.Indexer 79 | namespace string 80 | } 81 | 82 | // List lists all Components in the indexer for a given namespace. 83 | func (s componentNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Component, err error) { 84 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 85 | ret = append(ret, m.(*v1alpha1.Component)) 86 | }) 87 | return ret, err 88 | } 89 | 90 | // Get retrieves the Component from the indexer for a given namespace and name. 91 | func (s componentNamespaceLister) Get(name string) (*v1alpha1.Component, error) { 92 | obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) 93 | if err != nil { 94 | return nil, err 95 | } 96 | if !exists { 97 | return nil, errors.NewNotFound(v1alpha1.Resource("component"), name) 98 | } 99 | return obj.(*v1alpha1.Component), nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/operator/client/informers/externalversions/rusi/v1alpha1/configuration.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | "context" 23 | rusiv1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 24 | versioned "rusi/pkg/operator/client/clientset/versioned" 25 | internalinterfaces "rusi/pkg/operator/client/informers/externalversions/internalinterfaces" 26 | v1alpha1 "rusi/pkg/operator/client/listers/rusi/v1alpha1" 27 | time "time" 28 | 29 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | runtime "k8s.io/apimachinery/pkg/runtime" 31 | watch "k8s.io/apimachinery/pkg/watch" 32 | cache "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | // ConfigurationInformer provides access to a shared informer and lister for 36 | // Configurations. 37 | type ConfigurationInformer interface { 38 | Informer() cache.SharedIndexInformer 39 | Lister() v1alpha1.ConfigurationLister 40 | } 41 | 42 | type configurationInformer struct { 43 | factory internalinterfaces.SharedInformerFactory 44 | tweakListOptions internalinterfaces.TweakListOptionsFunc 45 | namespace string 46 | } 47 | 48 | // NewConfigurationInformer constructs a new informer for Configuration type. 49 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 50 | // one. This reduces memory footprint and number of connections to the server. 51 | func NewConfigurationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 52 | return NewFilteredConfigurationInformer(client, namespace, resyncPeriod, indexers, nil) 53 | } 54 | 55 | // NewFilteredConfigurationInformer constructs a new informer for Configuration type. 56 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 57 | // one. This reduces memory footprint and number of connections to the server. 58 | func NewFilteredConfigurationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 59 | return cache.NewSharedIndexInformer( 60 | &cache.ListWatch{ 61 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 62 | if tweakListOptions != nil { 63 | tweakListOptions(&options) 64 | } 65 | return client.RusiV1alpha1().Configurations(namespace).List(context.TODO(), options) 66 | }, 67 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 68 | if tweakListOptions != nil { 69 | tweakListOptions(&options) 70 | } 71 | return client.RusiV1alpha1().Configurations(namespace).Watch(context.TODO(), options) 72 | }, 73 | }, 74 | &rusiv1alpha1.Configuration{}, 75 | resyncPeriod, 76 | indexers, 77 | ) 78 | } 79 | 80 | func (f *configurationInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 81 | return NewFilteredConfigurationInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 82 | } 83 | 84 | func (f *configurationInformer) Informer() cache.SharedIndexInformer { 85 | return f.factory.InformerFor(&rusiv1alpha1.Configuration{}, f.defaultInformer) 86 | } 87 | 88 | func (f *configurationInformer) Lister() v1alpha1.ConfigurationLister { 89 | return v1alpha1.NewConfigurationLister(f.Informer().GetIndexer()) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/operator/client/listers/rusi/v1alpha1/configuration.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | v1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 23 | 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | "k8s.io/apimachinery/pkg/labels" 26 | "k8s.io/client-go/tools/cache" 27 | ) 28 | 29 | // ConfigurationLister helps list Configurations. 30 | // All objects returned here must be treated as read-only. 31 | type ConfigurationLister interface { 32 | // List lists all Configurations in the indexer. 33 | // Objects returned here must be treated as read-only. 34 | List(selector labels.Selector) (ret []*v1alpha1.Configuration, err error) 35 | // Configurations returns an object that can list and get Configurations. 36 | Configurations(namespace string) ConfigurationNamespaceLister 37 | ConfigurationListerExpansion 38 | } 39 | 40 | // configurationLister implements the ConfigurationLister interface. 41 | type configurationLister struct { 42 | indexer cache.Indexer 43 | } 44 | 45 | // NewConfigurationLister returns a new ConfigurationLister. 46 | func NewConfigurationLister(indexer cache.Indexer) ConfigurationLister { 47 | return &configurationLister{indexer: indexer} 48 | } 49 | 50 | // List lists all Configurations in the indexer. 51 | func (s *configurationLister) List(selector labels.Selector) (ret []*v1alpha1.Configuration, err error) { 52 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 53 | ret = append(ret, m.(*v1alpha1.Configuration)) 54 | }) 55 | return ret, err 56 | } 57 | 58 | // Configurations returns an object that can list and get Configurations. 59 | func (s *configurationLister) Configurations(namespace string) ConfigurationNamespaceLister { 60 | return configurationNamespaceLister{indexer: s.indexer, namespace: namespace} 61 | } 62 | 63 | // ConfigurationNamespaceLister helps list and get Configurations. 64 | // All objects returned here must be treated as read-only. 65 | type ConfigurationNamespaceLister interface { 66 | // List lists all Configurations in the indexer for a given namespace. 67 | // Objects returned here must be treated as read-only. 68 | List(selector labels.Selector) (ret []*v1alpha1.Configuration, err error) 69 | // Get retrieves the Configuration from the indexer for a given namespace and name. 70 | // Objects returned here must be treated as read-only. 71 | Get(name string) (*v1alpha1.Configuration, error) 72 | ConfigurationNamespaceListerExpansion 73 | } 74 | 75 | // configurationNamespaceLister implements the ConfigurationNamespaceLister 76 | // interface. 77 | type configurationNamespaceLister struct { 78 | indexer cache.Indexer 79 | namespace string 80 | } 81 | 82 | // List lists all Configurations in the indexer for a given namespace. 83 | func (s configurationNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Configuration, err error) { 84 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 85 | ret = append(ret, m.(*v1alpha1.Configuration)) 86 | }) 87 | return ret, err 88 | } 89 | 90 | // Get retrieves the Configuration from the indexer for a given namespace and name. 91 | func (s configurationNamespaceLister) Get(name string) (*v1alpha1.Configuration, error) { 92 | obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) 93 | if err != nil { 94 | return nil, err 95 | } 96 | if !exists { 97 | return nil, errors.NewNotFound(v1alpha1.Resource("configuration"), name) 98 | } 99 | return obj.(*v1alpha1.Configuration), nil 100 | } 101 | -------------------------------------------------------------------------------- /cmd/rusid/sidecar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net/http" 7 | "syscall" 8 | 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | "k8s.io/klog/v2" 11 | 12 | //_ "net/http/pprof" 13 | "os" 14 | "os/signal" 15 | "rusi/internal/diagnostics" 16 | "rusi/internal/metrics" 17 | "rusi/internal/tracing" 18 | "rusi/internal/version" 19 | grpc_api "rusi/pkg/api/runtime/grpc" 20 | components_loader "rusi/pkg/custom-resource/components/loader" 21 | configuration_loader "rusi/pkg/custom-resource/configuration/loader" 22 | "rusi/pkg/healthcheck" 23 | "rusi/pkg/modes" 24 | "rusi/pkg/operator" 25 | "rusi/pkg/runtime" 26 | "sync" 27 | "time" 28 | ) 29 | 30 | func main() { 31 | mainCtx, cancel := context.WithCancel(context.Background()) 32 | wg := &sync.WaitGroup{} 33 | 34 | //https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md 35 | klog.InitFlags(nil) 36 | defer klog.Flush() 37 | klog.InfoS("Rusid is starting") 38 | 39 | cfgBuilder := runtime.NewRuntimeConfigBuilder() 40 | cfgBuilder.AttachCmdFlags(flag.StringVar, flag.BoolVar, flag.IntVar) 41 | flag.Parse() 42 | cfg, err := cfgBuilder.Build() 43 | if err != nil { 44 | klog.Error(err) 45 | return 46 | } 47 | compLoader := components_loader.LoadLocalComponents(cfg.ComponentsPath) 48 | configLoader := configuration_loader.LoadStandaloneConfiguration(cfg.Config) 49 | if cfg.Mode == modes.KubernetesMode { 50 | compLoader = operator.GetComponentsWatcher(mainCtx, cfg.ControlPlaneAddress, wg) 51 | configLoader = operator.GetConfigurationWatcher(mainCtx, cfg.ControlPlaneAddress, cfg.Config, wg) 52 | klog.InfoS("KubernetesMode enabled") 53 | } 54 | 55 | //setup tracing 56 | go diagnostics.WatchConfig(mainCtx, configLoader, tracing.SetTracing(cfg.AppID)) 57 | klog.InfoS("Setup opentelemetry finished") 58 | 59 | compManager, err := runtime.NewComponentsManager(mainCtx, cfg.AppID, compLoader, 60 | RegisterComponentFactories()...) 61 | if err != nil { 62 | klog.Error(err) 63 | return 64 | } 65 | klog.InfoS("Components manager is running") 66 | 67 | api := grpc_api.NewGrpcAPI(cfg.RusiGRPCHost, cfg.RusiGRPCPort) 68 | klog.InfoS("Rusi grpc server is running") 69 | rt, err := runtime.NewRuntime(mainCtx, cfg, api, configLoader, compManager) 70 | if err != nil { 71 | klog.Error(err) 72 | return 73 | } 74 | 75 | klog.InfoS("Rusid is started", "host", cfg.RusiGRPCHost, "port", cfg.RusiGRPCPort, 76 | "app id", cfg.AppID, "mode", cfg.Mode, "version", version.Version(), 77 | "git commit", version.Commit(), "git version", version.GitVersion()) 78 | klog.InfoS("Rusid is using", "config", cfg) 79 | 80 | //Start diagnostics server 81 | go startDiagnosticsServer(mainCtx, wg, cfg.AppID, cfg.DiagnosticsPort, cfg.EnableMetrics, 82 | // WithTimeout allows you to set a max overall timeout. 83 | healthcheck.WithTimeout(5*time.Second), 84 | healthcheck.WithChecker("component manager", compManager), 85 | healthcheck.WithChecker("runtime", rt)) 86 | 87 | shutdownOnInterrupt(cancel) 88 | 89 | err = rt.Run(mainCtx) //blocks 90 | if err != nil { 91 | klog.Error(err) 92 | } 93 | 94 | wg.Wait() // wait for app to close gracefully 95 | klog.Info("Rusid closed gracefully") 96 | } 97 | 98 | func shutdownOnInterrupt(cancel func()) { 99 | c := make(chan os.Signal, 1) 100 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 101 | 102 | go func() { 103 | <-c 104 | klog.InfoS("Shutdown requested") 105 | cancel() 106 | }() 107 | } 108 | 109 | func startDiagnosticsServer(ctx context.Context, wg *sync.WaitGroup, appId string, port int, 110 | enableMetrics bool, options ...healthcheck.Option) { 111 | wg.Add(1) 112 | defer wg.Done() 113 | 114 | router := http.NewServeMux() 115 | router.Handle("/healthz", healthcheck.HandlerFunc(options...)) 116 | 117 | if enableMetrics { 118 | _ = metrics.SetupPrometheusMetrics(appId) 119 | router.Handle("/metrics", promhttp.Handler()) 120 | } else { 121 | metrics.SetNoopMeterProvider() 122 | } 123 | 124 | if err := diagnostics.Run(ctx, port, router); err != nil { 125 | if err != http.ErrServerClosed { 126 | klog.ErrorS(err, "failed to start diagnostics server") 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module rusi 2 | 3 | go 1.25.0 4 | 5 | //https://github.com/golang/go/wiki/Modules#how-to-upgrade-and-downgrade-dependencies 6 | 7 | require ( 8 | github.com/google/uuid v1.6.0 9 | github.com/json-iterator/go v1.1.12 10 | github.com/kelseyhightower/envconfig v1.4.0 11 | github.com/nats-io/nats-server/v2 v2.11.8 12 | github.com/nats-io/nats.go v1.45.0 13 | github.com/nats-io/stan.go v0.10.4 14 | github.com/pkg/errors v0.9.1 15 | github.com/prometheus/client_golang v1.21.1 16 | github.com/stretchr/testify v1.10.0 17 | go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 18 | go.opentelemetry.io/otel v1.35.0 19 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 20 | go.opentelemetry.io/otel/exporters/prometheus v0.57.0 21 | go.opentelemetry.io/otel/metric v1.35.0 22 | go.opentelemetry.io/otel/sdk v1.35.0 23 | go.opentelemetry.io/otel/sdk/metric v1.35.0 24 | go.opentelemetry.io/otel/trace v1.35.0 25 | golang.org/x/sync v0.16.0 26 | google.golang.org/grpc v1.71.0 27 | google.golang.org/protobuf v1.36.6 28 | gopkg.in/yaml.v2 v2.4.0 29 | k8s.io/api v0.32.3 30 | k8s.io/apiextensions-apiserver v0.32.3 31 | k8s.io/apimachinery v0.32.3 32 | k8s.io/client-go v0.32.3 33 | k8s.io/klog/v2 v2.130.1 34 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e 35 | ) 36 | 37 | require ( 38 | github.com/beorn7/perks v1.0.1 // indirect 39 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 40 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 41 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 42 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 43 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 44 | github.com/go-logr/logr v1.4.2 // indirect 45 | github.com/go-logr/stdr v1.2.2 // indirect 46 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 47 | github.com/go-openapi/jsonreference v0.21.0 // indirect 48 | github.com/go-openapi/swag v0.23.1 // indirect 49 | github.com/gogo/protobuf v1.3.2 // indirect 50 | github.com/golang/protobuf v1.5.4 // indirect 51 | github.com/google/gnostic-models v0.6.9 // indirect 52 | github.com/google/go-cmp v0.7.0 // indirect 53 | github.com/google/go-tpm v0.9.5 // indirect 54 | github.com/google/gofuzz v1.2.0 // indirect 55 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect 56 | github.com/josharian/intern v1.0.0 // indirect 57 | github.com/klauspost/compress v1.18.0 // indirect 58 | github.com/mailru/easyjson v0.9.0 // indirect 59 | github.com/minio/highwayhash v1.0.3 // indirect 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 61 | github.com/modern-go/reflect2 v1.0.2 // indirect 62 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 63 | github.com/nats-io/jwt/v2 v2.7.4 // indirect 64 | github.com/nats-io/nats-streaming-server v0.25.6 // indirect 65 | github.com/nats-io/nkeys v0.4.11 // indirect 66 | github.com/nats-io/nuid v1.0.1 // indirect 67 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 68 | github.com/prometheus/client_model v0.6.1 // indirect 69 | github.com/prometheus/common v0.63.0 // indirect 70 | github.com/prometheus/procfs v0.16.0 // indirect 71 | github.com/spf13/pflag v1.0.6 // indirect 72 | github.com/x448/float16 v0.8.4 // indirect 73 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 74 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect 75 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 76 | golang.org/x/crypto v0.41.0 // indirect 77 | golang.org/x/net v0.42.0 // indirect 78 | golang.org/x/oauth2 v0.28.0 // indirect 79 | golang.org/x/sys v0.35.0 // indirect 80 | golang.org/x/term v0.34.0 // indirect 81 | golang.org/x/text v0.28.0 // indirect 82 | golang.org/x/time v0.12.0 // indirect 83 | google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect 84 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect 85 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 86 | gopkg.in/inf.v0 v0.9.1 // indirect 87 | gopkg.in/yaml.v3 v3.0.1 // indirect 88 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 89 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 90 | sigs.k8s.io/randfill v1.0.0 // indirect 91 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 92 | sigs.k8s.io/yaml v1.4.0 // indirect 93 | ) 94 | -------------------------------------------------------------------------------- /pkg/operator/client.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "context" 5 | jsoniter "github.com/json-iterator/go" 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/credentials/insecure" 8 | "k8s.io/klog/v2" 9 | "rusi/internal/kube" 10 | "rusi/pkg/custom-resource/components" 11 | "rusi/pkg/custom-resource/configuration" 12 | operatorv1 "rusi/pkg/proto/operator/v1" 13 | "sync" 14 | ) 15 | 16 | func newClient(ctx context.Context, address string) (operatorv1.RusiOperatorClient, error) { 17 | var retryPolicy = `{ 18 | "methodConfig": [{ 19 | "name": [{"service": "rusi.proto.operator.v1.RusiOperator"}], 20 | "waitForReady": true, 21 | "retryPolicy": { 22 | "MaxAttempts": 4, 23 | "InitialBackoff": ".01s", 24 | "MaxBackoff": ".01s", 25 | "BackoffMultiplier": 1.0, 26 | "RetryableStatusCodes": [ "UNAVAILABLE" ] 27 | } 28 | }]}` 29 | 30 | conn, conErr := grpc.DialContext(ctx, address, 31 | grpc.WithTransportCredentials(insecure.NewCredentials()), 32 | grpc.WithDefaultServiceConfig(retryPolicy)) 33 | if conErr != nil { 34 | return nil, conErr 35 | } 36 | return operatorv1.NewRusiOperatorClient(conn), nil 37 | } 38 | 39 | func GetComponentsWatcher(ctx context.Context, address string, wg *sync.WaitGroup) func(context.Context) (<-chan components.Spec, error) { 40 | client, err := newClient(ctx, address) 41 | if err != nil { 42 | klog.ErrorS(err, "error creating grpc operator client") 43 | } 44 | return func(ctx context.Context) (<-chan components.Spec, error) { 45 | c := make(chan components.Spec) 46 | namespace := kube.GetCurrentNamespace() 47 | klog.V(4).InfoS("Got CurrentNamespace") 48 | req := &operatorv1.WatchComponentsRequest{Namespace: namespace} 49 | stream, err := client.WatchComponents(ctx, req) 50 | klog.V(4).InfoS("Got WatchComponents grpc stream") 51 | 52 | if err != nil { 53 | return nil, err 54 | } 55 | go func() { 56 | wg.Add(1) 57 | defer wg.Done() 58 | defer close(c) 59 | for { 60 | select { 61 | case <-ctx.Done(): 62 | klog.ErrorS(ctx.Err(), "watch components shutting down") 63 | return 64 | default: 65 | for { 66 | msg, err := stream.Recv() 67 | if err != nil { 68 | klog.ErrorS(err, "watch components grpc stream error") 69 | break 70 | } 71 | spec := components.Spec{} 72 | err = jsoniter.Unmarshal(msg.Data, &spec) 73 | if err != nil { 74 | klog.ErrorS(err, "unable to Unmarshal operator data ") 75 | } 76 | c <- spec 77 | } 78 | klog.Warning("watch components grpc stream closed, reconnecting...") 79 | stream, _ = client.WatchComponents(ctx, req) 80 | } 81 | } 82 | }() 83 | return c, nil 84 | } 85 | } 86 | 87 | func GetConfigurationWatcher(ctx context.Context, address, configName string, wg *sync.WaitGroup) func(context.Context) (<-chan configuration.Spec, error) { 88 | client, err := newClient(ctx, address) 89 | if err != nil { 90 | klog.ErrorS(err, "error creating grpc operator client") 91 | } 92 | return func(ctx context.Context) (<-chan configuration.Spec, error) { 93 | c := make(chan configuration.Spec) 94 | namespace := kube.GetCurrentNamespace() 95 | req := &operatorv1.WatchConfigurationRequest{ConfigName: configName, Namespace: namespace} 96 | stream, err := client.WatchConfiguration(ctx, req) 97 | if err != nil { 98 | return nil, err 99 | } 100 | go func() { 101 | wg.Add(1) 102 | defer wg.Done() 103 | defer close(c) 104 | for { 105 | select { 106 | case <-ctx.Done(): 107 | klog.ErrorS(ctx.Err(), "watch configuration shutting down") 108 | return 109 | default: 110 | for { 111 | msg, err := stream.Recv() 112 | if err != nil { 113 | klog.ErrorS(err, "watch configuration grpc stream error") 114 | break 115 | } 116 | spec := configuration.Spec{} 117 | err = jsoniter.Unmarshal(msg.Data, &spec) 118 | if err != nil { 119 | klog.ErrorS(err, "unable to Unmarshal operator data ") 120 | } 121 | c <- spec 122 | } 123 | klog.Warning("watch configuration grpc stream closed, reconnecting ...") 124 | stream, _ = client.WatchConfiguration(ctx, req) 125 | } 126 | } 127 | }() 128 | return c, nil 129 | } 130 | } 131 | 132 | //func IsOperatorClientAlive() bool { 133 | // return conn != nil && conn.GetState() == connectivity.Ready 134 | //} 135 | -------------------------------------------------------------------------------- /pkg/custom-resource/configuration/loader/local_test.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "context" 5 | "io/fs" 6 | "os" 7 | "rusi/pkg/custom-resource/configuration" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestLoadStandaloneConfiguration(t *testing.T) { 14 | writeFile("config.yaml", ` 15 | kind: Configuration 16 | metadata: 17 | name: secretappconfig 18 | spec: 19 | mtls: 20 | enabled: true 21 | features: 22 | - name: "feature1" 23 | enabled: true 24 | - name: "feature2" 25 | enabled: false 26 | pubSub: 27 | name: natsstreaming-pubsub`) 28 | 29 | writeFile("env_variables_config.yaml", ` 30 | kind: Configuration 31 | metadata: 32 | name: secretappconfig 33 | spec: 34 | telemetry: 35 | collectorEndpoint: "${RUSI_TELEMETRY_COLLECTOR_ENDPOINT}" 36 | tracing: 37 | propagator: w3c `) 38 | 39 | defer os.Remove("config.yaml") 40 | defer os.Remove("env_variables_config.yaml") 41 | 42 | testCases := []struct { 43 | name string 44 | path string 45 | errorExpected bool 46 | }{ 47 | { 48 | name: "Valid config file", 49 | path: "config.yaml", 50 | errorExpected: false, 51 | }, 52 | { 53 | name: "Invalid file path", 54 | path: "invalid_file.yaml", 55 | errorExpected: true, 56 | }, 57 | } 58 | 59 | ctx := context.Background() 60 | 61 | for _, tc := range testCases { 62 | t.Run(tc.name, func(t *testing.T) { 63 | _, err := LoadStandaloneConfiguration(tc.path)(ctx) 64 | if tc.errorExpected { 65 | assert.Error(t, err, "Expected an error") 66 | } else { 67 | assert.NoError(t, err, "Unexpected error") 68 | } 69 | }) 70 | } 71 | 72 | t.Run("Parse environment variables", func(t *testing.T) { 73 | os.Setenv("RUSI_TELEMETRY_COLLECTOR_ENDPOINT", "http://localhost:42323") 74 | configChan, err := LoadStandaloneConfiguration("env_variables_config.yaml")(ctx) 75 | config := <-configChan 76 | assert.NoError(t, err, "Unexpected error") 77 | assert.NotNil(t, config, "Config not loaded as expected") 78 | assert.Equal(t, "http://localhost:42323", config.Telemetry.CollectorEndpoint) 79 | }) 80 | 81 | t.Run("Load config file", func(t *testing.T) { 82 | configChan, err := LoadStandaloneConfiguration("config.yaml")(ctx) 83 | config := <-configChan 84 | assert.NoError(t, err, "Unexpected error") 85 | assert.NotNil(t, config, "Config not loaded as expected") 86 | assert.Len(t, config.Features, 2) 87 | assert.True(t, config.Features[0].Enabled) 88 | assert.False(t, config.Features[1].Enabled) 89 | assert.Equal(t, configuration.Feature("feature1"), config.Features[0].Name) 90 | assert.Equal(t, configuration.Feature("feature2"), config.Features[1].Name) 91 | assert.Equal(t, config.PubSubSpec.Name, "natsstreaming-pubsub") 92 | }) 93 | } 94 | 95 | func TestFeatureSpecForStandAlone(t *testing.T) { 96 | 97 | writeFile("feature_config.yaml", ` 98 | kind: Configuration 99 | metadata: 100 | name: configName 101 | spec: 102 | features: 103 | - name: Actor.Reentrancy 104 | enabled: true 105 | - name: Test.Feature 106 | enabled: false`) 107 | defer os.Remove("feature_config.yaml") 108 | 109 | testCases := []struct { 110 | name string 111 | confFile string 112 | featureName configuration.Feature 113 | featureEnabled bool 114 | }{ 115 | { 116 | name: "Feature is enabled", 117 | confFile: "feature_config.yaml", 118 | featureName: configuration.Feature("Actor.Reentrancy"), 119 | featureEnabled: true, 120 | }, 121 | { 122 | name: "Feature is disabled", 123 | confFile: "feature_config.yaml", 124 | featureName: configuration.Feature("Test.Feature"), 125 | featureEnabled: false, 126 | }, 127 | { 128 | name: "Feature is disabled if missing", 129 | confFile: "feature_config.yaml", 130 | featureName: configuration.Feature("Test.Missing"), 131 | featureEnabled: false, 132 | }, 133 | } 134 | ctx := context.Background() 135 | for _, tc := range testCases { 136 | t.Run(tc.name, func(t *testing.T) { 137 | configChan, err := LoadStandaloneConfiguration(tc.confFile)(ctx) 138 | config := <-configChan 139 | assert.NoError(t, err) 140 | assert.Equal(t, tc.featureEnabled, configuration.IsFeatureEnabled(config.Features, tc.featureName)) 141 | }) 142 | } 143 | } 144 | 145 | func writeFile(path, content string) { 146 | _ = os.WriteFile(path, []byte(content), fs.FileMode(0644)) 147 | } 148 | -------------------------------------------------------------------------------- /helm/charts/rusi_sidecar_injector/templates/sidecar_injector_deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: rusi-sidecar-injector 5 | labels: 6 | app: rusi-sidecar-injector 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: rusi-sidecar-injector 12 | template: 13 | metadata: 14 | labels: 15 | app: rusi-sidecar-injector 16 | app.kubernetes.io/name: {{ .Release.Name }} 17 | app.kubernetes.io/version: {{ .Values.global.tag }} 18 | app.kubernetes.io/component: sidecar-injector 19 | app.kubernetes.io/part-of: "rusi" 20 | app.kubernetes.io/managed-by: "helm" 21 | {{- if eq .Values.global.prometheus.enabled true }} 22 | annotations: 23 | prometheus.io/scrape: "{{ .Values.global.prometheus.enabled }}" 24 | prometheus.io/port: "{{ .Values.global.prometheus.port }}" 25 | prometheus.io/path: "/" 26 | {{- end }} 27 | spec: 28 | serviceAccountName: rusi-operator 29 | containers: 30 | - name: rusi-sidecar-injector 31 | {{- if contains "/" .Values.injectorImage.name }} 32 | image: "{{ .Values.injectorImage.name }}" 33 | {{- else }} 34 | image: "{{ .Values.global.registry }}/rusi:{{ .Values.global.tag }}" 35 | {{- end }} 36 | imagePullPolicy: {{ .Values.global.imagePullPolicy }} 37 | securityContext: 38 | {{- if eq .Values.runAsNonRoot true }} 39 | runAsNonRoot: {{ .Values.runAsNonRoot }} 40 | {{- else }} 41 | runAsUser: 1000 42 | {{- end }} 43 | {{- if eq .Values.debug.enabled true }} 44 | capabilities: 45 | add: 46 | - SYS_PTRACE 47 | {{- end }} 48 | command: 49 | {{- if eq .Values.debug.enabled false }} 50 | - "/injector" 51 | {{- else }} 52 | - "/dlv" 53 | {{- end }} 54 | args: 55 | {{- if eq .Values.debug.enabled true }} 56 | - "--listen=:{{ .Values.debug.port }}" 57 | - "--accept-multiclient" 58 | - "--headless=true" 59 | - "--log" 60 | - "--api-version=2" 61 | - "exec" 62 | - "/injector" 63 | - "--" 64 | {{- end }} 65 | - "--v" 66 | - "{{ .Values.logLevel }}" 67 | - "--validate_service_account=false" 68 | {{- if eq .Values.global.prometheus.enabled true }} 69 | - "--enable-metrics" 70 | - "--metrics-port" 71 | - "{{ .Values.global.prometheus.port }}" 72 | {{- else }} 73 | #- "--enable-metrics=false" 74 | {{- end }} 75 | env: 76 | - name: TLS_CERT_FILE 77 | value: /rusi/cert/tls.crt 78 | - name: TLS_KEY_FILE 79 | value: /rusi/cert/tls.key 80 | {{- if .Values.kubeClusterDomain }} 81 | - name: KUBE_CLUSTER_DOMAIN 82 | value: "{{ .Values.kubeClusterDomain }}" 83 | {{- end }} 84 | - name: SIDECAR_IMAGE 85 | {{- if contains "/" .Values.image.name }} 86 | value: "{{ .Values.image.name }}" 87 | {{- else }} 88 | value: "{{ .Values.global.registry }}/rusid:{{ .Values.global.tag }}" 89 | {{- end }} 90 | - name: SIDECAR_IMAGE_PULL_POLICY 91 | value: "{{ .Values.sidecarImagePullPolicy }}" 92 | - name: SIDECAR_IMAGE_PULL_SECRETS 93 | value: "{{ .Values.global.imagePullSecrets }}" 94 | - name: NAMESPACE 95 | valueFrom: 96 | fieldRef: 97 | fieldPath: metadata.namespace 98 | ports: 99 | - name: https 100 | containerPort: 4000 101 | protocol: TCP 102 | {{- if eq .Values.global.prometheus.enabled true }} 103 | - name: metrics 104 | containerPort: {{ .Values.global.prometheus.port }} 105 | protocol: TCP 106 | {{- end }} 107 | {{- if eq .Values.debug.enabled true }} 108 | - name: debug 109 | containerPort: {{ .Values.debug.port }} 110 | protocol: TCP 111 | {{- end }} 112 | resources: 113 | {{ toYaml .Values.resources | indent 10 }} 114 | volumeMounts: 115 | - name: cert 116 | mountPath: /rusi/cert 117 | readOnly: true 118 | volumes: 119 | - name: cert 120 | secret: 121 | secretName: rusi-sidecar-injector-cert 122 | {{- if .Values.global.imagePullSecrets }} 123 | imagePullSecrets: 124 | - name: {{ .Values.global.imagePullSecrets }} 125 | {{- end }} 126 | {{- if .Values.global.nodeSelector }} 127 | nodeSelector: 128 | {{ toYaml .Values.global.nodeSelector | indent 8 }} 129 | {{- end }} 130 | {{- if .Values.webhookHostNetwork }} 131 | hostNetwork: true 132 | {{- end }} 133 | -------------------------------------------------------------------------------- /pkg/middleware/tracing.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "rusi/pkg/messaging" 7 | 8 | "go.opentelemetry.io/otel" 9 | "go.opentelemetry.io/otel/attribute" 10 | "go.opentelemetry.io/otel/baggage" 11 | "go.opentelemetry.io/otel/codes" 12 | semconv "go.opentelemetry.io/otel/semconv/v1.17.0" 13 | "go.opentelemetry.io/otel/trace" 14 | "k8s.io/klog/v2" 15 | "k8s.io/utils/strings" 16 | ) 17 | 18 | func PublisherTracingMiddleware() messaging.Middleware { 19 | tr := otel.Tracer("tracing-middleware") 20 | 21 | return func(next messaging.Handler) messaging.Handler { 22 | return func(ctx context.Context, msg *messaging.MessageEnvelope) error { 23 | bags, spanCtx := Extract(ctx, msg.Headers) 24 | ctx = baggage.ContextWithBaggage(ctx, bags) 25 | topic := ctx.Value(messaging.TopicKey).(string) 26 | 27 | // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md 28 | ctx, span := tr.Start( 29 | trace.ContextWithRemoteSpanContext(ctx, spanCtx), 30 | fmt.Sprintf("%s send", topic), 31 | trace.WithSpanKind(trace.SpanKindProducer), 32 | trace.WithAttributes( 33 | attribute.String("component", "Rusi"), 34 | semconv.MessagingDestinationName(topic), 35 | semconv.MessagingDestinationKindTopic)) 36 | 37 | defer span.End() 38 | 39 | Inject(ctx, msg.Headers) 40 | klog.V(4).InfoS("publisher tracing middleware") 41 | err := next(ctx, msg) 42 | 43 | if err == nil { 44 | span.SetStatus(codes.Ok, "") 45 | } else { 46 | span.SetStatus(codes.Error, fmt.Sprintf("%v", err)) 47 | span.RecordError(err, trace.WithStackTrace(true)) 48 | } 49 | 50 | return err 51 | } 52 | } 53 | } 54 | 55 | func SubscriberTracingMiddleware() messaging.Middleware { 56 | tr := otel.Tracer("tracing-middleware") 57 | 58 | return func(next messaging.Handler) messaging.Handler { 59 | return func(ctx context.Context, msg *messaging.MessageEnvelope) error { 60 | 61 | bags, spanCtx := Extract(ctx, msg.Headers) 62 | ctx = baggage.ContextWithBaggage(ctx, bags) 63 | topic := ctx.Value(messaging.TopicKey).(string) 64 | 65 | // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md 66 | ctx, span := tr.Start( 67 | trace.ContextWithRemoteSpanContext(ctx, spanCtx), 68 | fmt.Sprintf("%s receive", topic), 69 | trace.WithSpanKind(trace.SpanKindConsumer), 70 | trace.WithAttributes( 71 | attribute.String("component", "Rusi"), 72 | semconv.MessagingDestinationName(topic), 73 | semconv.MessagingDestinationKindTopic, 74 | semconv.MessagingOperationReceive)) 75 | 76 | span.AddEvent("new message received", 77 | trace.WithAttributes(attribute.String("headers", fmt.Sprintf("%v", msg.Headers)))) 78 | 79 | if msg.Payload != nil { 80 | str := fmt.Sprintf("%v", msg.Payload) 81 | span.SetAttributes(attribute.Key("messaging.message_payload").String(strings.ShortenString(str, 500))) 82 | } 83 | span.SetAttributes(attribute.Key("messaging.message_id").String(msg.Id)) 84 | 85 | Inject(ctx, msg.Headers) 86 | 87 | defer span.End() 88 | klog.V(4).InfoS("subscriber tracing middleware") 89 | err := next(ctx, msg) 90 | 91 | if err == nil { 92 | span.SetStatus(codes.Ok, "") 93 | } else { 94 | span.SetStatus(codes.Error, fmt.Sprintf("%v", err)) 95 | span.RecordError(err, trace.WithStackTrace(true)) 96 | } 97 | 98 | return err 99 | } 100 | } 101 | } 102 | 103 | type mapHeaderCarrier struct { 104 | innerMap map[string]string 105 | } 106 | 107 | func (i *mapHeaderCarrier) Get(key string) string { 108 | return i.innerMap[key] 109 | } 110 | func (i *mapHeaderCarrier) Set(key string, value string) { 111 | i.innerMap[key] = value 112 | } 113 | func (i *mapHeaderCarrier) Keys() []string { 114 | keys := make([]string, 0, len(i.innerMap)) 115 | for k := range i.innerMap { 116 | keys = append(keys, k) 117 | } 118 | return keys 119 | } 120 | 121 | // Inject injects correlation context and span context into the gRPC 122 | // metadata object. This function is meant to be used on outgoing 123 | // requests. 124 | func Inject(ctx context.Context, headers map[string]string) { 125 | otel.GetTextMapPropagator().Inject(ctx, &mapHeaderCarrier{headers}) 126 | } 127 | 128 | // Extract returns the correlation context and span context that 129 | // another service encoded in the gRPC metadata object with Inject. 130 | // This function is meant to be used on incoming requests. 131 | func Extract(ctx context.Context, headers map[string]string) (baggage.Baggage, trace.SpanContext) { 132 | ctx = otel.GetTextMapPropagator().Extract(ctx, &mapHeaderCarrier{headers}) 133 | return baggage.FromContext(ctx), trace.SpanContextFromContext(ctx) 134 | } 135 | -------------------------------------------------------------------------------- /helm/crds/rusi.io_configurations.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.17.1 7 | name: configurations.rusi.io 8 | spec: 9 | group: rusi.io 10 | names: 11 | kind: Configuration 12 | listKind: ConfigurationList 13 | plural: configurations 14 | singular: configuration 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Configuration describes an Rusi configuration setting. 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: ConfigurationSpec is the spec for an configuration. 41 | properties: 42 | features: 43 | items: 44 | description: FeatureSpec defines the features that are enabled/disabled. 45 | properties: 46 | enabled: 47 | type: boolean 48 | name: 49 | type: string 50 | required: 51 | - enabled 52 | - name 53 | type: object 54 | type: array 55 | minRuntimeVersion: 56 | type: string 57 | pubSub: 58 | description: PubSubSpec defines default pubSub configuration. 59 | properties: 60 | name: 61 | type: string 62 | required: 63 | - name 64 | type: object 65 | publisherPipeline: 66 | description: PipelineSpec defines the middleware pipeline. 67 | properties: 68 | handlers: 69 | items: 70 | description: HandlerSpec defines a request handlers. 71 | properties: 72 | name: 73 | type: string 74 | type: 75 | type: string 76 | required: 77 | - name 78 | - type 79 | type: object 80 | type: array 81 | required: 82 | - handlers 83 | type: object 84 | subscriberPipeline: 85 | description: PipelineSpec defines the middleware pipeline. 86 | properties: 87 | handlers: 88 | items: 89 | description: HandlerSpec defines a request handlers. 90 | properties: 91 | name: 92 | type: string 93 | type: 94 | type: string 95 | required: 96 | - name 97 | - type 98 | type: object 99 | type: array 100 | required: 101 | - handlers 102 | type: object 103 | telemetry: 104 | description: Telemetry related configuration. 105 | properties: 106 | collectorEndpoint: 107 | description: Telemetry collector enpoint address. 108 | type: string 109 | tracing: 110 | description: Tracing configuration. 111 | properties: 112 | propagator: 113 | default: w3c 114 | description: 'Telemetry propagator. Possible values: w3c, 115 | jaeger' 116 | enum: 117 | - w3c 118 | - jaeger 119 | type: string 120 | required: 121 | - propagator 122 | type: object 123 | required: 124 | - collectorEndpoint 125 | type: object 126 | type: object 127 | type: object 128 | served: true 129 | storage: true 130 | -------------------------------------------------------------------------------- /pkg/custom-resource/components/loader/local.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "rusi/pkg/custom-resource/components" 13 | "strings" 14 | 15 | "github.com/google/uuid" 16 | "github.com/pkg/errors" 17 | yaml "gopkg.in/yaml.v2" 18 | "k8s.io/klog/v2" 19 | ) 20 | 21 | const ( 22 | yamlSeparator = "\n---" 23 | componentKind = "Component" 24 | ) 25 | 26 | type yamlComponent struct { 27 | Kind string `yaml:"kind,omitempty"` 28 | Spec yamlSpec `yaml:"spec"` 29 | Metadata metaData `yaml:"metadata"` 30 | Scopes []string `yaml:"scopes"` 31 | } 32 | 33 | type metaData struct { 34 | Name string `yaml:"name"` 35 | } 36 | 37 | type yamlSpec struct { 38 | Type string `yaml:"type"` 39 | Version string `yaml:"version"` 40 | Metadata []metadataItem `yaml:"metadata"` 41 | } 42 | 43 | type metadataItem struct { 44 | Name string `json:"name"` 45 | Value string `json:"value,omitempty"` 46 | } 47 | 48 | // LoadLocalComponents loads rusi components from a given directory. 49 | func LoadLocalComponents(componentsPath string) ComponentsLoader { 50 | return func(ctx context.Context) (<-chan components.Spec, error) { 51 | c := make(chan components.Spec) 52 | 53 | files, err := ioutil.ReadDir(componentsPath) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | var list []components.Spec 59 | 60 | for _, file := range files { 61 | if !file.IsDir() && isYaml(file.Name()) { 62 | path := filepath.Join(componentsPath, file.Name()) 63 | comps, _ := loadComponentsFromFile(path) 64 | if len(comps) > 0 { 65 | list = append(list, comps...) 66 | } 67 | } 68 | } 69 | 70 | //send to channel 71 | 72 | go func() { 73 | for _, comp := range list { 74 | c <- comp 75 | } 76 | }() 77 | 78 | return c, nil 79 | } 80 | } 81 | 82 | func loadComponentsFromFile(path string) ([]components.Spec, error) { 83 | var errs []error 84 | var comps []components.Spec 85 | b, err := ioutil.ReadFile(path) 86 | if err != nil { 87 | klog.Errorf("load components error when reading file %s : %s", path, err) 88 | return comps, err 89 | } 90 | 91 | b = []byte(os.ExpandEnv(string(b))) 92 | 93 | comps, errs = decodeYaml(b) 94 | for _, err := range errs { 95 | errStr := fmt.Sprintf("load components error when parsing components yaml resource in %s : %s", path, err) 96 | klog.Error(errStr) 97 | err = errors.Wrap(err, errStr) 98 | } 99 | return comps, err 100 | } 101 | 102 | // isYaml checks whether the file is yaml or not. 103 | func isYaml(fileName string) bool { 104 | extension := strings.ToLower(filepath.Ext(fileName)) 105 | if extension == ".yaml" || extension == ".yml" { 106 | return true 107 | } 108 | return false 109 | } 110 | 111 | // decodeYaml decodes the yaml document. 112 | func decodeYaml(b []byte) (list []components.Spec, errs []error) { 113 | scanner := bufio.NewScanner(bytes.NewReader(b)) 114 | scanner.Split(splitYamlDoc) 115 | 116 | for { 117 | var comp yamlComponent 118 | err := decode(scanner, &comp) 119 | if err == io.EOF { 120 | break 121 | } 122 | 123 | if err != nil { 124 | errs = append(errs, err) 125 | continue 126 | } 127 | 128 | if comp.Kind != componentKind { 129 | continue 130 | } 131 | 132 | list = append(list, components.Spec{ 133 | Name: comp.Metadata.Name, 134 | Type: comp.Spec.Type, 135 | Version: comp.Spec.Version, 136 | Metadata: convertMetadataItemsToProperties(comp.Spec.Metadata), 137 | Scopes: comp.Scopes, 138 | }) 139 | } 140 | 141 | return 142 | } 143 | 144 | // decode reads the YAML resource in document. 145 | func decode(scanner *bufio.Scanner, c interface{}) error { 146 | if scanner.Scan() { 147 | return yaml.Unmarshal(scanner.Bytes(), c) 148 | } 149 | 150 | err := scanner.Err() 151 | if err == nil { 152 | err = io.EOF 153 | } 154 | return err 155 | } 156 | 157 | // splitYamlDoc - splits the yaml docs. 158 | func splitYamlDoc(data []byte, atEOF bool) (advance int, token []byte, err error) { 159 | if atEOF && len(data) == 0 { 160 | return 0, nil, nil 161 | } 162 | sep := len([]byte(yamlSeparator)) 163 | if i := bytes.Index(data, []byte(yamlSeparator)); i >= 0 { 164 | i += sep 165 | after := data[i:] 166 | 167 | if len(after) == 0 { 168 | if atEOF { 169 | return len(data), data[:len(data)-sep], nil 170 | } 171 | return 0, nil, nil 172 | } 173 | if j := bytes.IndexByte(after, '\n'); j >= 0 { 174 | return i + j + 1, data[0 : i-sep], nil 175 | } 176 | return 0, nil, nil 177 | } 178 | // If we're at EOF, we have a final, non-terminated line. Return it. 179 | if atEOF { 180 | return len(data), data, nil 181 | } 182 | // Request more data. 183 | return 0, nil, nil 184 | } 185 | 186 | func convertMetadataItemsToProperties(items []metadataItem) map[string]string { 187 | properties := map[string]string{} 188 | for _, c := range items { 189 | val := c.Value 190 | for strings.Contains(val, "{uuid}") { 191 | val = strings.Replace(val, "{uuid}", uuid.New().String(), 1) 192 | } 193 | properties[c.Name] = val 194 | } 195 | return properties 196 | } 197 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1/fake/fake_component.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | "context" 23 | v1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 24 | 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | labels "k8s.io/apimachinery/pkg/labels" 27 | schema "k8s.io/apimachinery/pkg/runtime/schema" 28 | types "k8s.io/apimachinery/pkg/types" 29 | watch "k8s.io/apimachinery/pkg/watch" 30 | testing "k8s.io/client-go/testing" 31 | ) 32 | 33 | // FakeComponents implements ComponentInterface 34 | type FakeComponents struct { 35 | Fake *FakeRusiV1alpha1 36 | ns string 37 | } 38 | 39 | var componentsResource = schema.GroupVersionResource{Group: "rusi.io", Version: "v1alpha1", Resource: "components"} 40 | 41 | var componentsKind = schema.GroupVersionKind{Group: "rusi.io", Version: "v1alpha1", Kind: "Component"} 42 | 43 | // Get takes name of the component, and returns the corresponding component object, and an error if there is any. 44 | func (c *FakeComponents) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Component, err error) { 45 | obj, err := c.Fake. 46 | Invokes(testing.NewGetAction(componentsResource, c.ns, name), &v1alpha1.Component{}) 47 | 48 | if obj == nil { 49 | return nil, err 50 | } 51 | return obj.(*v1alpha1.Component), err 52 | } 53 | 54 | // List takes label and field selectors, and returns the list of Components that match those selectors. 55 | func (c *FakeComponents) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ComponentList, err error) { 56 | obj, err := c.Fake. 57 | Invokes(testing.NewListAction(componentsResource, componentsKind, c.ns, opts), &v1alpha1.ComponentList{}) 58 | 59 | if obj == nil { 60 | return nil, err 61 | } 62 | 63 | label, _, _ := testing.ExtractFromListOptions(opts) 64 | if label == nil { 65 | label = labels.Everything() 66 | } 67 | list := &v1alpha1.ComponentList{ListMeta: obj.(*v1alpha1.ComponentList).ListMeta} 68 | for _, item := range obj.(*v1alpha1.ComponentList).Items { 69 | if label.Matches(labels.Set(item.Labels)) { 70 | list.Items = append(list.Items, item) 71 | } 72 | } 73 | return list, err 74 | } 75 | 76 | // Watch returns a watch.Interface that watches the requested components. 77 | func (c *FakeComponents) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 78 | return c.Fake. 79 | InvokesWatch(testing.NewWatchAction(componentsResource, c.ns, opts)) 80 | 81 | } 82 | 83 | // Create takes the representation of a component and creates it. Returns the server's representation of the component, and an error, if there is any. 84 | func (c *FakeComponents) Create(ctx context.Context, component *v1alpha1.Component, opts v1.CreateOptions) (result *v1alpha1.Component, err error) { 85 | obj, err := c.Fake. 86 | Invokes(testing.NewCreateAction(componentsResource, c.ns, component), &v1alpha1.Component{}) 87 | 88 | if obj == nil { 89 | return nil, err 90 | } 91 | return obj.(*v1alpha1.Component), err 92 | } 93 | 94 | // Update takes the representation of a component and updates it. Returns the server's representation of the component, and an error, if there is any. 95 | func (c *FakeComponents) Update(ctx context.Context, component *v1alpha1.Component, opts v1.UpdateOptions) (result *v1alpha1.Component, err error) { 96 | obj, err := c.Fake. 97 | Invokes(testing.NewUpdateAction(componentsResource, c.ns, component), &v1alpha1.Component{}) 98 | 99 | if obj == nil { 100 | return nil, err 101 | } 102 | return obj.(*v1alpha1.Component), err 103 | } 104 | 105 | // Delete takes name of the component and deletes it. Returns an error if one occurs. 106 | func (c *FakeComponents) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 107 | _, err := c.Fake. 108 | Invokes(testing.NewDeleteAction(componentsResource, c.ns, name), &v1alpha1.Component{}) 109 | 110 | return err 111 | } 112 | 113 | // DeleteCollection deletes a collection of objects. 114 | func (c *FakeComponents) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 115 | action := testing.NewDeleteCollectionAction(componentsResource, c.ns, listOpts) 116 | 117 | _, err := c.Fake.Invokes(action, &v1alpha1.ComponentList{}) 118 | return err 119 | } 120 | 121 | // Patch applies the patch and returns the patched component. 122 | func (c *FakeComponents) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Component, err error) { 123 | obj, err := c.Fake. 124 | Invokes(testing.NewPatchSubresourceAction(componentsResource, c.ns, name, pt, data, subresources...), &v1alpha1.Component{}) 125 | 126 | if obj == nil { 127 | return nil, err 128 | } 129 | return obj.(*v1alpha1.Component), err 130 | } 131 | -------------------------------------------------------------------------------- /pkg/operator/client/clientset/versioned/typed/rusi/v1alpha1/fake/fake_configuration.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | "context" 23 | v1alpha1 "rusi/pkg/operator/apis/rusi/v1alpha1" 24 | 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | labels "k8s.io/apimachinery/pkg/labels" 27 | schema "k8s.io/apimachinery/pkg/runtime/schema" 28 | types "k8s.io/apimachinery/pkg/types" 29 | watch "k8s.io/apimachinery/pkg/watch" 30 | testing "k8s.io/client-go/testing" 31 | ) 32 | 33 | // FakeConfigurations implements ConfigurationInterface 34 | type FakeConfigurations struct { 35 | Fake *FakeRusiV1alpha1 36 | ns string 37 | } 38 | 39 | var configurationsResource = schema.GroupVersionResource{Group: "rusi.io", Version: "v1alpha1", Resource: "configurations"} 40 | 41 | var configurationsKind = schema.GroupVersionKind{Group: "rusi.io", Version: "v1alpha1", Kind: "Configuration"} 42 | 43 | // Get takes name of the configuration, and returns the corresponding configuration object, and an error if there is any. 44 | func (c *FakeConfigurations) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Configuration, err error) { 45 | obj, err := c.Fake. 46 | Invokes(testing.NewGetAction(configurationsResource, c.ns, name), &v1alpha1.Configuration{}) 47 | 48 | if obj == nil { 49 | return nil, err 50 | } 51 | return obj.(*v1alpha1.Configuration), err 52 | } 53 | 54 | // List takes label and field selectors, and returns the list of Configurations that match those selectors. 55 | func (c *FakeConfigurations) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ConfigurationList, err error) { 56 | obj, err := c.Fake. 57 | Invokes(testing.NewListAction(configurationsResource, configurationsKind, c.ns, opts), &v1alpha1.ConfigurationList{}) 58 | 59 | if obj == nil { 60 | return nil, err 61 | } 62 | 63 | label, _, _ := testing.ExtractFromListOptions(opts) 64 | if label == nil { 65 | label = labels.Everything() 66 | } 67 | list := &v1alpha1.ConfigurationList{ListMeta: obj.(*v1alpha1.ConfigurationList).ListMeta} 68 | for _, item := range obj.(*v1alpha1.ConfigurationList).Items { 69 | if label.Matches(labels.Set(item.Labels)) { 70 | list.Items = append(list.Items, item) 71 | } 72 | } 73 | return list, err 74 | } 75 | 76 | // Watch returns a watch.Interface that watches the requested configurations. 77 | func (c *FakeConfigurations) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 78 | return c.Fake. 79 | InvokesWatch(testing.NewWatchAction(configurationsResource, c.ns, opts)) 80 | 81 | } 82 | 83 | // Create takes the representation of a configuration and creates it. Returns the server's representation of the configuration, and an error, if there is any. 84 | func (c *FakeConfigurations) Create(ctx context.Context, configuration *v1alpha1.Configuration, opts v1.CreateOptions) (result *v1alpha1.Configuration, err error) { 85 | obj, err := c.Fake. 86 | Invokes(testing.NewCreateAction(configurationsResource, c.ns, configuration), &v1alpha1.Configuration{}) 87 | 88 | if obj == nil { 89 | return nil, err 90 | } 91 | return obj.(*v1alpha1.Configuration), err 92 | } 93 | 94 | // Update takes the representation of a configuration and updates it. Returns the server's representation of the configuration, and an error, if there is any. 95 | func (c *FakeConfigurations) Update(ctx context.Context, configuration *v1alpha1.Configuration, opts v1.UpdateOptions) (result *v1alpha1.Configuration, err error) { 96 | obj, err := c.Fake. 97 | Invokes(testing.NewUpdateAction(configurationsResource, c.ns, configuration), &v1alpha1.Configuration{}) 98 | 99 | if obj == nil { 100 | return nil, err 101 | } 102 | return obj.(*v1alpha1.Configuration), err 103 | } 104 | 105 | // Delete takes name of the configuration and deletes it. Returns an error if one occurs. 106 | func (c *FakeConfigurations) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 107 | _, err := c.Fake. 108 | Invokes(testing.NewDeleteAction(configurationsResource, c.ns, name), &v1alpha1.Configuration{}) 109 | 110 | return err 111 | } 112 | 113 | // DeleteCollection deletes a collection of objects. 114 | func (c *FakeConfigurations) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 115 | action := testing.NewDeleteCollectionAction(configurationsResource, c.ns, listOpts) 116 | 117 | _, err := c.Fake.Invokes(action, &v1alpha1.ConfigurationList{}) 118 | return err 119 | } 120 | 121 | // Patch applies the patch and returns the patched configuration. 122 | func (c *FakeConfigurations) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Configuration, err error) { 123 | obj, err := c.Fake. 124 | Invokes(testing.NewPatchSubresourceAction(configurationsResource, c.ns, name, pt, data, subresources...), &v1alpha1.Configuration{}) 125 | 126 | if obj == nil { 127 | return nil, err 128 | } 129 | return obj.(*v1alpha1.Configuration), err 130 | } 131 | -------------------------------------------------------------------------------- /pkg/custom-resource/components/loader/local_test.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | ) 10 | 11 | const configPrefix = "." 12 | 13 | func writeTempConfig(path, content string) error { 14 | return os.WriteFile(filepath.Join(configPrefix, path), []byte(content), fs.FileMode(0644)) 15 | } 16 | 17 | func TestLoadComponentsFromFile(t *testing.T) { 18 | 19 | t.Run("valid yaml content", func(t *testing.T) { 20 | filename := "test-component-valid.yaml" 21 | yaml := ` 22 | kind: Component 23 | metadata: 24 | name: statestore 25 | spec: 26 | type: state.couchbase 27 | metadata: 28 | - name: prop1 29 | value: value1 30 | - name: prop2 31 | value: value2 32 | ` 33 | err := writeTempConfig(filename, yaml) 34 | defer os.Remove(filename) 35 | assert.NoError(t, err, "Unexpected error") 36 | components, err := loadComponentsFromFile(filename) 37 | assert.Len(t, components, 1) 38 | assert.NoError(t, err, "Unexpected error") 39 | }) 40 | 41 | t.Run("invalid yaml head", func(t *testing.T) { 42 | filename := "test-component-invalid.yaml" 43 | yaml := ` 44 | INVALID_YAML_HERE 45 | kind: Component 46 | metadata: 47 | name: statestore` 48 | err := writeTempConfig(filename, yaml) 49 | defer os.Remove(filename) 50 | assert.NoError(t, err, "Unexpected error") 51 | components, err := loadComponentsFromFile(filename) 52 | assert.Len(t, components, 0) 53 | assert.NoError(t, err, "Unexpected error") 54 | }) 55 | 56 | t.Run("load components file not exist", func(t *testing.T) { 57 | filename := "test-component-no-exist.yaml" 58 | 59 | components, err := loadComponentsFromFile(filename) 60 | assert.Len(t, components, 0) 61 | assert.Error(t, err, "Expected an error") 62 | }) 63 | } 64 | 65 | func TestIsYaml(t *testing.T) { 66 | 67 | assert.True(t, isYaml("test.yaml")) 68 | assert.True(t, isYaml("test.YAML")) 69 | assert.True(t, isYaml("test.yml")) 70 | assert.True(t, isYaml("test.YML")) 71 | assert.False(t, isYaml("test.md")) 72 | assert.False(t, isYaml("test.txt")) 73 | assert.False(t, isYaml("test.sh")) 74 | } 75 | 76 | func TestStandaloneDecodeValidYaml(t *testing.T) { 77 | yaml := ` 78 | kind: Component 79 | metadata: 80 | name: statestore 81 | spec: 82 | type: state.couchbase 83 | metadata: 84 | - name: prop1 85 | value: value1 86 | - name: prop2 87 | value: value2 88 | ` 89 | components, errs := decodeYaml([]byte(yaml)) 90 | assert.Len(t, components, 1) 91 | assert.Empty(t, errs) 92 | assert.Equal(t, "statestore", components[0].Name) 93 | assert.Equal(t, "state.couchbase", components[0].Type) 94 | assert.Len(t, components[0].Metadata, 2) 95 | assert.Equal(t, "value1", components[0].Metadata["prop1"]) 96 | } 97 | 98 | func TestStandaloneDecodeInvalidComponent(t *testing.T) { 99 | yaml := ` 100 | kind: Subscription 101 | metadata: 102 | name: testsub 103 | spec: 104 | metadata: 105 | - name: prop1 106 | value: value1 107 | - name: prop2 108 | value: value2 109 | ` 110 | components, errs := decodeYaml([]byte(yaml)) 111 | assert.Len(t, components, 0) 112 | assert.Len(t, errs, 0) 113 | } 114 | 115 | func TestStandaloneDecodeUnsuspectingFile(t *testing.T) { 116 | components, errs := decodeYaml([]byte("hey there")) 117 | assert.Len(t, components, 0) 118 | assert.Len(t, errs, 1) 119 | } 120 | 121 | func TestStandaloneDecodeInvalidYaml(t *testing.T) { 122 | 123 | yaml := ` 124 | INVALID_YAML_HERE 125 | kind: Component 126 | metadata: 127 | name: statestore` 128 | components, errs := decodeYaml([]byte(yaml)) 129 | assert.Len(t, components, 0) 130 | assert.Len(t, errs, 1) 131 | } 132 | 133 | func TestStandaloneDecodeValidMultiYaml(t *testing.T) { 134 | yaml := ` 135 | kind: Component 136 | metadata: 137 | name: statestore1 138 | spec: 139 | type: state.couchbase 140 | metadata: 141 | - name: prop1 142 | value: value1 143 | - name: prop2 144 | value: value2 145 | --- 146 | kind: Component 147 | metadata: 148 | name: statestore2 149 | spec: 150 | type: state.redis 151 | metadata: 152 | - name: prop3 153 | value: value3 154 | ` 155 | components, errs := decodeYaml([]byte(yaml)) 156 | assert.Len(t, components, 2) 157 | assert.Empty(t, errs) 158 | assert.Equal(t, "statestore1", components[0].Name) 159 | assert.Equal(t, "state.couchbase", components[0].Type) 160 | assert.Len(t, components[0].Metadata, 2) 161 | assert.Equal(t, "value1", components[0].Metadata["prop1"]) 162 | assert.Equal(t, "value2", components[0].Metadata["prop2"]) 163 | 164 | assert.Equal(t, "statestore2", components[1].Name) 165 | assert.Equal(t, "state.redis", components[1].Type) 166 | assert.Len(t, components[1].Metadata, 1) 167 | assert.Equal(t, "value3", components[1].Metadata["prop3"]) 168 | } 169 | 170 | func TestStandaloneDecodeInValidDocInMultiYaml(t *testing.T) { 171 | 172 | yaml := ` 173 | kind: Component 174 | metadata: 175 | name: statestore1 176 | spec: 177 | type: state.couchbase 178 | metadata: 179 | - name: prop1 180 | value: value1 181 | - name: prop2 182 | value: value2 183 | --- 184 | INVALID_YAML_HERE 185 | kind: Component 186 | metadata: 187 | name: invalidyaml 188 | --- 189 | kind: Component 190 | metadata: 191 | name: statestore2 192 | spec: 193 | type: state.redis 194 | metadata: 195 | - name: prop3 196 | value: value3 197 | ` 198 | components, errs := decodeYaml([]byte(yaml)) 199 | assert.Len(t, components, 2) 200 | assert.Len(t, errs, 1) 201 | 202 | assert.Equal(t, "statestore1", components[0].Name) 203 | assert.Equal(t, "state.couchbase", components[0].Type) 204 | assert.Len(t, components[0].Metadata, 2) 205 | assert.Equal(t, "value1", components[0].Metadata["prop1"]) 206 | assert.Equal(t, "value2", components[0].Metadata["prop2"]) 207 | 208 | assert.Equal(t, "statestore2", components[1].Name) 209 | assert.Equal(t, "state.redis", components[1].Type) 210 | assert.Len(t, components[1].Metadata, 1) 211 | assert.Equal(t, "value3", components[1].Metadata["prop3"]) 212 | } 213 | -------------------------------------------------------------------------------- /pkg/proto/runtime/v1/rusi_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | emptypb "google.golang.org/protobuf/types/known/emptypb" 11 | ) 12 | 13 | // This is a compile-time assertion to ensure that this generated file 14 | // is compatible with the grpc package it is being compiled against. 15 | // Requires gRPC-Go v1.32.0 or later. 16 | const _ = grpc.SupportPackageIsVersion7 17 | 18 | // RusiClient is the client API for Rusi service. 19 | // 20 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 21 | type RusiClient interface { 22 | // Publishes events to the specific topic. 23 | Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) 24 | // Subscribe pushes events on the stream 25 | Subscribe(ctx context.Context, opts ...grpc.CallOption) (Rusi_SubscribeClient, error) 26 | } 27 | 28 | type rusiClient struct { 29 | cc grpc.ClientConnInterface 30 | } 31 | 32 | func NewRusiClient(cc grpc.ClientConnInterface) RusiClient { 33 | return &rusiClient{cc} 34 | } 35 | 36 | func (c *rusiClient) Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 37 | out := new(emptypb.Empty) 38 | err := c.cc.Invoke(ctx, "/rusi.proto.runtime.v1.Rusi/Publish", in, out, opts...) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return out, nil 43 | } 44 | 45 | func (c *rusiClient) Subscribe(ctx context.Context, opts ...grpc.CallOption) (Rusi_SubscribeClient, error) { 46 | stream, err := c.cc.NewStream(ctx, &Rusi_ServiceDesc.Streams[0], "/rusi.proto.runtime.v1.Rusi/Subscribe", opts...) 47 | if err != nil { 48 | return nil, err 49 | } 50 | x := &rusiSubscribeClient{stream} 51 | return x, nil 52 | } 53 | 54 | type Rusi_SubscribeClient interface { 55 | Send(*SubscribeRequest) error 56 | Recv() (*ReceivedMessage, error) 57 | grpc.ClientStream 58 | } 59 | 60 | type rusiSubscribeClient struct { 61 | grpc.ClientStream 62 | } 63 | 64 | func (x *rusiSubscribeClient) Send(m *SubscribeRequest) error { 65 | return x.ClientStream.SendMsg(m) 66 | } 67 | 68 | func (x *rusiSubscribeClient) Recv() (*ReceivedMessage, error) { 69 | m := new(ReceivedMessage) 70 | if err := x.ClientStream.RecvMsg(m); err != nil { 71 | return nil, err 72 | } 73 | return m, nil 74 | } 75 | 76 | // RusiServer is the server API for Rusi service. 77 | // All implementations should embed UnimplementedRusiServer 78 | // for forward compatibility 79 | type RusiServer interface { 80 | // Publishes events to the specific topic. 81 | Publish(context.Context, *PublishRequest) (*emptypb.Empty, error) 82 | // Subscribe pushes events on the stream 83 | Subscribe(Rusi_SubscribeServer) error 84 | } 85 | 86 | // UnimplementedRusiServer should be embedded to have forward compatible implementations. 87 | type UnimplementedRusiServer struct { 88 | } 89 | 90 | func (UnimplementedRusiServer) Publish(context.Context, *PublishRequest) (*emptypb.Empty, error) { 91 | return nil, status.Errorf(codes.Unimplemented, "method Publish not implemented") 92 | } 93 | func (UnimplementedRusiServer) Subscribe(Rusi_SubscribeServer) error { 94 | return status.Errorf(codes.Unimplemented, "method Subscribe not implemented") 95 | } 96 | 97 | // UnsafeRusiServer may be embedded to opt out of forward compatibility for this service. 98 | // Use of this interface is not recommended, as added methods to RusiServer will 99 | // result in compilation errors. 100 | type UnsafeRusiServer interface { 101 | mustEmbedUnimplementedRusiServer() 102 | } 103 | 104 | func RegisterRusiServer(s grpc.ServiceRegistrar, srv RusiServer) { 105 | s.RegisterService(&Rusi_ServiceDesc, srv) 106 | } 107 | 108 | func _Rusi_Publish_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 109 | in := new(PublishRequest) 110 | if err := dec(in); err != nil { 111 | return nil, err 112 | } 113 | if interceptor == nil { 114 | return srv.(RusiServer).Publish(ctx, in) 115 | } 116 | info := &grpc.UnaryServerInfo{ 117 | Server: srv, 118 | FullMethod: "/rusi.proto.runtime.v1.Rusi/Publish", 119 | } 120 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 121 | return srv.(RusiServer).Publish(ctx, req.(*PublishRequest)) 122 | } 123 | return interceptor(ctx, in, info, handler) 124 | } 125 | 126 | func _Rusi_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error { 127 | return srv.(RusiServer).Subscribe(&rusiSubscribeServer{stream}) 128 | } 129 | 130 | type Rusi_SubscribeServer interface { 131 | Send(*ReceivedMessage) error 132 | Recv() (*SubscribeRequest, error) 133 | grpc.ServerStream 134 | } 135 | 136 | type rusiSubscribeServer struct { 137 | grpc.ServerStream 138 | } 139 | 140 | func (x *rusiSubscribeServer) Send(m *ReceivedMessage) error { 141 | return x.ServerStream.SendMsg(m) 142 | } 143 | 144 | func (x *rusiSubscribeServer) Recv() (*SubscribeRequest, error) { 145 | m := new(SubscribeRequest) 146 | if err := x.ServerStream.RecvMsg(m); err != nil { 147 | return nil, err 148 | } 149 | return m, nil 150 | } 151 | 152 | // Rusi_ServiceDesc is the grpc.ServiceDesc for Rusi service. 153 | // It's only intended for direct use with grpc.RegisterService, 154 | // and not to be introspected or modified (even as a copy) 155 | var Rusi_ServiceDesc = grpc.ServiceDesc{ 156 | ServiceName: "rusi.proto.runtime.v1.Rusi", 157 | HandlerType: (*RusiServer)(nil), 158 | Methods: []grpc.MethodDesc{ 159 | { 160 | MethodName: "Publish", 161 | Handler: _Rusi_Publish_Handler, 162 | }, 163 | }, 164 | Streams: []grpc.StreamDesc{ 165 | { 166 | StreamName: "Subscribe", 167 | Handler: _Rusi_Subscribe_Handler, 168 | ServerStreams: true, 169 | ClientStreams: true, 170 | }, 171 | }, 172 | Metadata: "proto/runtime/v1/rusi.proto", 173 | } 174 | -------------------------------------------------------------------------------- /pkg/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "rusi/internal/version" 8 | runtime_api "rusi/pkg/api/runtime" 9 | "rusi/pkg/custom-resource/components" 10 | "rusi/pkg/custom-resource/configuration" 11 | configuration_loader "rusi/pkg/custom-resource/configuration/loader" 12 | "rusi/pkg/healthcheck" 13 | "rusi/pkg/messaging" 14 | "rusi/pkg/middleware" 15 | "rusi/pkg/runtime/service" 16 | "time" 17 | 18 | "github.com/google/uuid" 19 | "github.com/pkg/errors" 20 | "k8s.io/klog/v2" 21 | ) 22 | 23 | type runtime struct { 24 | ctx context.Context 25 | appID string 26 | api runtime_api.Api 27 | 28 | appConfig configuration.Spec 29 | componentsManager *ComponentsManager 30 | 31 | configurationUpdatesChan <-chan configuration.Spec 32 | } 33 | 34 | func NewRuntime(ctx context.Context, config Config, api runtime_api.Api, 35 | configurationLoader configuration_loader.ConfigurationLoader, 36 | manager *ComponentsManager) (*runtime, error) { 37 | 38 | klog.InfoS("Loading configuration") 39 | configChan, err := configurationLoader(ctx) 40 | if err != nil { 41 | klog.ErrorS(err, "error loading application config", "name", 42 | config.Config, "mode", config.Mode) 43 | return nil, err 44 | } 45 | 46 | //block until config arrives 47 | klog.InfoS("Waiting for configuration changes") 48 | rt := &runtime{ 49 | api: api, 50 | ctx: ctx, 51 | appID: config.AppID, 52 | 53 | configurationUpdatesChan: configChan, 54 | appConfig: <-configChan, 55 | componentsManager: manager, 56 | } 57 | 58 | api.SetPublishHandler(rt.PublishHandler) 59 | api.SetSubscribeHandler(rt.SubscribeHandler) 60 | 61 | go rt.watchComponentsUpdates() 62 | go rt.watchConfigurationUpdates() 63 | 64 | return rt, nil 65 | } 66 | 67 | func (rt *runtime) watchConfigurationUpdates() { 68 | for update := range rt.configurationUpdatesChan { 69 | if reflect.DeepEqual(rt.appConfig, update) { 70 | klog.V(4).InfoS("configuration not changed") 71 | continue 72 | } 73 | klog.InfoS("configuration changed") 74 | klog.V(4).InfoS("configuration details", "old", rt.appConfig, "new", update) 75 | rt.appConfig = update 76 | err := rt.api.Refresh() 77 | if err != nil { 78 | klog.ErrorS(err, "error refreshing subscription") 79 | } 80 | } 81 | } 82 | 83 | func (rt *runtime) watchComponentsUpdates() { 84 | for update := range rt.componentsManager.Watch() { 85 | klog.InfoS("component changed", "operation", update.Operation, 86 | "name", update.ComponentSpec.Name, "type", update.ComponentSpec.Type) 87 | 88 | switch { 89 | //case update.ComponentCategory == components.PubsubComponent && update.Operation == components.Update: 90 | case update.Operation == components.Update: 91 | err := rt.api.Refresh() 92 | if err != nil { 93 | klog.ErrorS(err, "error refreshing subscription") 94 | } 95 | } 96 | } 97 | } 98 | 99 | func (rt *runtime) buildSubscriberPipeline() (pipeline messaging.Pipeline, err error) { 100 | for _, middlewareSpec := range rt.appConfig.SubscriberPipelineSpec.Handlers { 101 | midlw, err := rt.componentsManager.GetMiddleware(middlewareSpec) 102 | if err != nil { 103 | return pipeline, err 104 | } 105 | klog.Infof("enabled %s/%s middleware", middlewareSpec.Type, middlewareSpec.Version) 106 | pipeline.UseMiddleware(midlw) 107 | } 108 | return pipeline, nil 109 | } 110 | 111 | func (rt *runtime) PublishHandler(ctx context.Context, request messaging.PublishRequest) error { 112 | var pubSubName string 113 | if request.PubsubName != "" { 114 | pubSubName = request.PubsubName 115 | } else { 116 | pubSubName = rt.appConfig.PubSubSpec.Name 117 | } 118 | if pubSubName == "" { 119 | return errors.New("PubSubName is empty. Please provide a valid pubSub name in the publish request or via configuration.") 120 | } 121 | 122 | publisher := rt.componentsManager.GetPublisher(pubSubName) 123 | if publisher == nil { 124 | return errors.New(fmt.Sprintf(runtime_api.ErrPubsubNotFound, pubSubName)) 125 | } 126 | env := &messaging.MessageEnvelope{ 127 | Id: uuid.New().String(), 128 | Time: time.Now(), 129 | Subject: request.Topic, 130 | Type: request.Type, 131 | DataContentType: request.DataContentType, 132 | Headers: request.Metadata, 133 | Payload: request.Data, 134 | } 135 | 136 | ctx = context.WithValue(ctx, messaging.TopicKey, request.Topic) 137 | 138 | pipe := messaging.Pipeline{} 139 | pipe.UseMiddleware(middleware.PublisherTracingMiddleware()) 140 | pipe.UseMiddleware(middleware.PublisherMetricsMiddleware()) 141 | 142 | midl := pipe.Build(func(ctx context.Context, msg *messaging.MessageEnvelope) error { 143 | return publisher.Publish(request.Topic, msg) 144 | }) 145 | 146 | return midl(ctx, env) 147 | } 148 | 149 | func (rt *runtime) SubscribeHandler(ctx context.Context, request messaging.SubscribeRequest) (messaging.CloseFunc, error) { 150 | var pubSubName string 151 | if request.PubsubName != "" { 152 | pubSubName = request.PubsubName 153 | } else { 154 | pubSubName = rt.appConfig.PubSubSpec.Name 155 | } 156 | if pubSubName == "" { 157 | return nil, errors.New("PubSubName is empty. Please provide a valid pubSub name in the subscribe request or via configuration.") 158 | } 159 | 160 | subs := rt.componentsManager.GetSubscriber(pubSubName) 161 | if subs == nil { 162 | err := errors.New(fmt.Sprintf("cannot find PubsubName named %s", pubSubName)) 163 | klog.ErrorS(err, err.Error()) 164 | return nil, err 165 | } 166 | pipeline, err := rt.buildSubscriberPipeline() 167 | if err != nil { 168 | klog.ErrorS(err, "error building pipeline") 169 | return nil, err 170 | } 171 | srv := service.NewSubscriberService(subs, pipeline) 172 | return srv.StartSubscribing(request) 173 | } 174 | 175 | func (rt *runtime) Run(ctx context.Context) error { 176 | return rt.api.Serve(ctx) 177 | } 178 | 179 | func (rt *runtime) IsHealthy(ctx context.Context) healthcheck.HealthResult { 180 | if rt.appConfig.MinRuntimeVersion != "" && version.Version() < rt.appConfig.MinRuntimeVersion { 181 | return healthcheck.HealthResult{ 182 | Status: healthcheck.Unhealthy, 183 | Description: "a bigger minimum runtime version is required", 184 | } 185 | } 186 | return healthcheck.HealthyResult 187 | } 188 | --------------------------------------------------------------------------------