├── hack ├── custom-boilerplate.go.txt ├── verify-codegen.sh └── update-codegen.sh ├── .gitattributes ├── charts ├── sso-operator │ ├── README.md │ ├── templates │ │ ├── NOTES.txt │ │ ├── sa.yaml │ │ ├── rolebinding.yaml │ │ ├── _helpers.tpl │ │ ├── service.yaml │ │ ├── cert-grpc-client.yaml │ │ ├── sso-crd.yaml │ │ ├── role.yaml │ │ └── deployment.yaml │ ├── Chart.yaml │ ├── .helmignore │ ├── values.yaml │ └── Makefile └── preview │ ├── Chart.yaml │ ├── requirements.yaml │ ├── values.yaml │ └── Makefile ├── images └── architecture.png ├── watch.sh ├── .gitignore ├── pkg ├── apis │ └── jenkins.io │ │ ├── v1 │ │ ├── doc.go │ │ ├── register.go │ │ ├── types.go │ │ └── zz_generated.deepcopy.go │ │ └── register.go ├── client │ ├── clientset │ │ └── versioned │ │ │ ├── doc.go │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ └── clientset_generated.go │ │ │ ├── typed │ │ │ └── jenkins.io │ │ │ │ └── v1 │ │ │ │ ├── generated_expansion.go │ │ │ │ ├── doc.go │ │ │ │ ├── fake │ │ │ │ ├── doc.go │ │ │ │ ├── fake_jenkins.io_client.go │ │ │ │ └── fake_sso.go │ │ │ │ ├── jenkins.io_client.go │ │ │ │ └── sso.go │ │ │ ├── scheme │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ └── clientset.go │ ├── listers │ │ └── jenkins.io │ │ │ └── v1 │ │ │ ├── expansion_generated.go │ │ │ └── sso.go │ └── informers │ │ └── externalversions │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ ├── jenkins.io │ │ ├── v1 │ │ │ ├── interface.go │ │ │ └── sso.go │ │ └── interface.go │ │ ├── generic.go │ │ └── factory.go ├── proxy │ ├── expose_test.go │ ├── config_test.go │ ├── proxy_test.go │ ├── config.go │ ├── expose.go │ └── proxy.go ├── kubernetes │ ├── sso.go │ ├── ingress.go │ ├── crds.go │ ├── client.go │ ├── rbac.go │ └── wait.go ├── operator │ ├── config.go │ └── operator.go └── dex │ └── client.go ├── curlloop.sh ├── .pre-commit-config.yaml ├── OWNERS ├── .dockerignore ├── Dockerfile ├── .helmignore ├── examples └── sso.yaml ├── Makefile ├── jenkins-x-whitesource.yml ├── .secrets.baseline ├── go.mod ├── jenkins-x.yml ├── main.go ├── README.md ├── .whitesource.config └── LICENSE /hack/custom-boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | Makefile linguist-vendored 2 | -------------------------------------------------------------------------------- /charts/sso-operator/README.md: -------------------------------------------------------------------------------- 1 | # sso-operator 2 | 3 | Helm chart for sso-operator. 4 | -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccojocar/sso-operator/HEAD/images/architecture.png -------------------------------------------------------------------------------- /watch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | make skaffold-run 4 | reflex -r "\.go$" -R "vendor.*" make skaffold-run 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .classpath 3 | .idea 4 | .cache 5 | .DS_Store 6 | *.im? 7 | target 8 | work 9 | bin/ 10 | vendor 11 | -------------------------------------------------------------------------------- /charts/sso-operator/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 2 | Get the application URL by running these commands: 3 | 4 | kubectl get ingress {{ template "fullname" . }} 5 | -------------------------------------------------------------------------------- /pkg/apis/jenkins.io/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | 3 | // Package v1 is the v1 version of the API. 4 | // +groupName=jenkins.io 5 | package v1 6 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated clientset. 4 | package versioned 5 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated fake clientset. 4 | package fake 5 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/jenkins.io/v1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | type SSOExpansion interface{} 6 | -------------------------------------------------------------------------------- /curlloop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export APP="$1" 4 | echo "curling URL $APP in a loop..." 5 | 6 | while true 7 | do 8 | curl $APP 9 | sleep 2 10 | done 11 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/jenkins.io/v1/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated typed clients. 4 | package v1 5 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/jenkins.io/v1/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // Package fake has the automatically generated clients. 4 | package fake 5 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package contains the scheme of the automatically generated clientset. 4 | package scheme 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: git@github.com:Yelp/detect-secrets 3 | rev: v0.12.4 4 | hooks: 5 | - id: detect-secrets 6 | args: ['--baseline', '.secrets.baseline'] 7 | exclude: .*/tests/.* -------------------------------------------------------------------------------- /charts/sso-operator/templates/sa.yaml: -------------------------------------------------------------------------------- 1 | kind: ServiceAccount 2 | apiVersion: v1 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 7 | -------------------------------------------------------------------------------- /charts/preview/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: A Helm chart for Kubernetes 3 | icon: https://raw.githubusercontent.com/jenkins-x/jenkins-x-platform/master/images/go.png 4 | name: preview 5 | version: 0.1.0-SNAPSHOT 6 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - rawlingsj 3 | - jstrachan 4 | - ccojocar 5 | - garethjevans 6 | - pmuir 7 | - abayer 8 | reviewers: 9 | - rawlingsj 10 | - jstrachan 11 | - ccojocar 12 | - garethjevans 13 | - pmuir 14 | - abayer 15 | -------------------------------------------------------------------------------- /charts/sso-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: A Helm chart for sso-operator 3 | icon: https://raw.githubusercontent.com/jenkins-x/jenkins-x-platform/d273e09/images/go.png 4 | name: sso-operator 5 | version: 1.2.19 6 | appVersion: 1.2.20 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | draft.toml 2 | target/classes 3 | target/generated-sources 4 | target/generated-test-sources 5 | target/maven-archiver 6 | target/maven-status 7 | target/surefire-reports 8 | target/test-classes 9 | target/*.original 10 | charts/ 11 | NOTICE 12 | LICENSE 13 | README.md -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.2 2 | 3 | COPY . /go/src/github.com/jenkins-x/sso-operator 4 | WORKDIR /go/src/github.com/jenkins-x/sso-operator 5 | RUN make build 6 | 7 | FROM scratch 8 | COPY --from=0 /go/src/github.com/jenkins-x/sso-operator/bin/sso-operator /sso-operator 9 | EXPOSE 8080 10 | ENTRYPOINT ["/sso-operator"] 11 | -------------------------------------------------------------------------------- /pkg/apis/jenkins.io/register.go: -------------------------------------------------------------------------------- 1 | package jenkinsio 2 | 3 | const ( 4 | // GroupName is the Jenkins API group name 5 | GroupName = "jenkins.io" 6 | // Version is the Jenkins API group version 7 | Version = "v1" 8 | 9 | // GroupAndVersion is the Jenkins API Group and version 10 | GroupAndVersion = GroupName + "/" + Version 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/client/listers/jenkins.io/v1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by lister-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | // SSOListerExpansion allows custom methods to be added to 6 | // SSOLister. 7 | type SSOListerExpansion interface{} 8 | 9 | // SSONamespaceListerExpansion allows custom methods to be added to 10 | // SSONamespaceLister. 11 | type SSONamespaceListerExpansion interface{} 12 | -------------------------------------------------------------------------------- /charts/preview/requirements.yaml: -------------------------------------------------------------------------------- 1 | 2 | dependencies: 3 | - alias: expose 4 | name: exposecontroller 5 | repository: https://chartmuseum.build.cd.jenkins-x.io 6 | version: 2.3.67 7 | - alias: cleanup 8 | name: exposecontroller 9 | repository: https://chartmuseum.build.cd.jenkins-x.io 10 | version: 2.3.67 11 | - alias: preview 12 | name: sso-operator 13 | repository: file://../sso-operator 14 | -------------------------------------------------------------------------------- /charts/sso-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 | -------------------------------------------------------------------------------- /charts/sso-operator/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 7 | subjects: 8 | - kind: ServiceAccount 9 | name: {{ template "fullname" . }} 10 | namespace: {{ .Release.Namespace }} 11 | roleRef: 12 | kind: ClusterRole 13 | name: {{ template "fullname" . }} 14 | apiGroup: rbac.authorization.k8s.io 15 | 16 | -------------------------------------------------------------------------------- /.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 | *.png 23 | 24 | # known compile time folders 25 | target/ 26 | node_modules/ 27 | vendor/ -------------------------------------------------------------------------------- /charts/preview/values.yaml: -------------------------------------------------------------------------------- 1 | 2 | expose: 3 | Annotations: 4 | helm.sh/hook: post-install,post-upgrade 5 | helm.sh/hook-delete-policy: hook-succeeded 6 | config: 7 | exposer: Ingress 8 | http: true 9 | tlsacme: false 10 | 11 | cleanup: 12 | Args: 13 | - --cleanup 14 | Annotations: 15 | helm.sh/hook: pre-delete 16 | helm.sh/hook-delete-policy: hook-succeeded 17 | 18 | preview: 19 | image: 20 | repo: gcr.io/jenkinsxio/sso-operator 21 | tag: 22 | pullPolicy: IfNotPresent 23 | -------------------------------------------------------------------------------- /pkg/proxy/expose_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestExposeConfig(t *testing.T) { 10 | config := &ExposeConfig{ 11 | Domain: "test", 12 | Exposer: "Ingress", 13 | PathMode: "", 14 | HTTP: true, 15 | TLSAcme: false, 16 | Services: []string{"test"}, 17 | } 18 | 19 | strConfig, err := renderExposeConfig(config) 20 | 21 | assert.NoError(t, err, "should render expose config to YAML without error") 22 | assert.NotEmpty(t, strConfig, "expose config should not be empty") 23 | } 24 | -------------------------------------------------------------------------------- /charts/sso-operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /charts/sso-operator/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | {{- if .Values.service.name }} 5 | name: {{ .Values.service.name }} 6 | {{- else }} 7 | name: {{ template "fullname" . }} 8 | {{- end }} 9 | labels: 10 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 11 | {{- if .Values.service.annotations }} 12 | annotations: 13 | {{ toYaml .Values.service.annotations | indent 4 }} 14 | {{- end }} 15 | spec: 16 | type: {{ .Values.service.type }} 17 | ports: 18 | - port: {{ .Values.service.externalPort }} 19 | targetPort: {{ .Values.service.internalPort }} 20 | protocol: TCP 21 | name: http 22 | selector: 23 | app: {{ template "fullname" . }} 24 | -------------------------------------------------------------------------------- /charts/sso-operator/templates/cert-grpc-client.yaml: -------------------------------------------------------------------------------- 1 | {{ $fullname := include "fullname" . }} 2 | {{- if .Values.certs.legacyApi }} 3 | apiVersion: certmanager.k8s.io/v1alpha1 4 | {{- else }} 5 | apiVersion: cert-manager.io/v1alpha2 6 | {{- end }} 7 | kind: Certificate 8 | metadata: 9 | name: {{ $fullname }}-grpc-client-cert 10 | labels: 11 | app: {{ $fullname }} 12 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 13 | spec: 14 | secretName: {{ .Values.dex.certs.grpc.client.secretName }} 15 | issuerRef: 16 | name: {{ .Values.dex.certs.grpc.issuer.name }} 17 | kind: {{ .Values.dex.certs.grpc.issuer.kind }} 18 | commonName: dex-grpc-client 19 | dnsName: 20 | - {{ .Values.dex.grpcHost }} 21 | -------------------------------------------------------------------------------- /examples/sso.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: "jenkins.io/v1" 2 | kind: "SSO" 3 | metadata: 4 | name: "sso-golang-http" 5 | namespace: jx-staging 6 | spec: 7 | oidcIssuerUrl: "https://dex.jx-staging.exmaple.com" 8 | upstreamService: "golang-http" 9 | forwardToken: true 10 | domain: "exmaple.com" 11 | certIssuerName: "letsencrypt-prod" 12 | urlTemplate: "{{.Service}}.{{.Namespace}}.{{.Domain}}" 13 | cookieSpec: 14 | name: "sso-golang-http" 15 | expire: "168h" 16 | refresh: "60m" 17 | secure: true 18 | httpOnly: true 19 | proxyImage: "quay.io/pusher/oauth2_proxy" 20 | proxyImageTag: "v3.2.0" 21 | proxyResources: 22 | limits: 23 | cpu: 100m 24 | memory: 256Mi 25 | requests: 26 | cpu: 80m 27 | memory: 128Mi 28 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/jenkins.io/v1/fake/fake_jenkins.io_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | v1 "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned/typed/jenkins.io/v1" 7 | rest "k8s.io/client-go/rest" 8 | testing "k8s.io/client-go/testing" 9 | ) 10 | 11 | type FakeJenkinsV1 struct { 12 | *testing.Fake 13 | } 14 | 15 | func (c *FakeJenkinsV1) SSOs(namespace string) v1.SSOInterface { 16 | return &FakeSSOs{c, namespace} 17 | } 18 | 19 | // RESTClient returns a RESTClient that is used to communicate 20 | // with API server by this client implementation. 21 | func (c *FakeJenkinsV1) RESTClient() rest.Interface { 22 | var ret *rest.RESTClient 23 | return ret 24 | } 25 | -------------------------------------------------------------------------------- /charts/preview/Makefile: -------------------------------------------------------------------------------- 1 | OS := $(shell uname) 2 | 3 | preview: 4 | ifeq ($(OS),Darwin) 5 | sed -i "" -e "s/version:.*/version: $(PREVIEW_VERSION)/" Chart.yaml 6 | sed -i "" -e "s/version:.*/version: $(PREVIEW_VERSION)/" ../*/Chart.yaml 7 | sed -i "" -e "s/tag: .*/tag: $(PREVIEW_VERSION)/" values.yaml 8 | else ifeq ($(OS),Linux) 9 | sed -i -e "s/version:.*/version: $(PREVIEW_VERSION)/" Chart.yaml 10 | sed -i -e "s/version:.*/version: $(PREVIEW_VERSION)/" ../*/Chart.yaml 11 | sed -i -e "s|repository: .*|repository: $(DOCKER_REGISTRY)\/jenkins-x\/sso-operator|" values.yaml 12 | sed -i -e "s/tag: .*/tag: $(PREVIEW_VERSION)/" values.yaml 13 | else 14 | echo "platfrom $(OS) not supported to release from" 15 | exit -1 16 | endif 17 | echo " version: $(PREVIEW_VERSION)" >> requirements.yaml 18 | jx step helm build 19 | -------------------------------------------------------------------------------- /pkg/kubernetes/sso.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 5 | "github.com/pkg/errors" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | // IsSSOInitialized checks if the SSO is initialized by retrieving the current state from k8s 10 | func IsSSOInitialized(sso *v1.SSO) (bool, error) { 11 | client, err := GetJenkinsClient() 12 | if err != nil { 13 | return false, errors.Wrap(err, "getting Jenkins client") 14 | } 15 | 16 | ssos, err := client.JenkinsV1().SSOs(sso.GetNamespace()).List(metav1.ListOptions{}) 17 | if err != nil { 18 | return false, errors.Wrap(err, "listing SSO resources") 19 | } 20 | 21 | for _, s := range ssos.Items { 22 | if s.GetName() == sso.GetName() { 23 | return s.Status.Initialized, nil 24 | } 25 | } 26 | 27 | return false, nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package internalinterfaces 4 | 5 | import ( 6 | time "time" 7 | 8 | versioned "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | cache "k8s.io/client-go/tools/cache" 12 | ) 13 | 14 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 15 | 16 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 17 | type SharedInformerFactory interface { 18 | Start(stopCh <-chan struct{}) 19 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 20 | } 21 | 22 | type TweakListOptionsFunc func(*v1.ListOptions) 23 | -------------------------------------------------------------------------------- /pkg/proxy/config_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestProxyConfig(t *testing.T) { 10 | config := &Config{ 11 | Port: 4180, 12 | ClientID: "123", 13 | ClientSecret: "test", 14 | OIDCIssuerURL: "http://test-issuer", 15 | RedirectURL: "http://test-proxy/calback", 16 | LoginURL: "http://test-proxy/auth", 17 | RedeemURL: "http://test-proxy/token", 18 | Upstream: "http://test-upstream", 19 | ForwardToken: false, 20 | Cookie: Cookie{ 21 | Name: "test-cookie", 22 | Secret: "test", 23 | Domain: "http://test-proxy", 24 | Expire: "168h0m", 25 | Refresh: "60m", 26 | Secure: true, 27 | HTTPOnly: true, 28 | }, 29 | } 30 | 31 | strConfig, err := renderConfig(config) 32 | 33 | assert.NoError(t, err, "should render proxy config without error") 34 | assert.NotEmpty(t, strConfig, "proxy config should not be empty") 35 | } 36 | -------------------------------------------------------------------------------- /pkg/kubernetes/ingress.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | // FindIngressHosts searches an ingress resource by name and retrieves its hosts 11 | func FindIngressHosts(name string, namespace string) ([]string, error) { 12 | k8sClient, err := GetClientset() 13 | if err != nil { 14 | return nil, errors.Wrap(err, "getting k8s client") 15 | } 16 | 17 | ingresses, err := k8sClient.Extensions().Ingresses(namespace).List(metav1.ListOptions{}) 18 | if err != nil { 19 | return nil, errors.Wrap(err, "listing ingress resources") 20 | } 21 | 22 | for _, ingress := range ingresses.Items { 23 | if ingress.GetName() == name { 24 | hosts := []string{} 25 | rules := ingress.Spec.Rules 26 | for _, rule := range rules { 27 | hosts = append(hosts, rule.Host) 28 | } 29 | return hosts, nil 30 | } 31 | } 32 | return nil, fmt.Errorf("ingress '%s' not found", name) 33 | } 34 | -------------------------------------------------------------------------------- /charts/sso-operator/templates/sso-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: ssos.jenkins.io 5 | spec: 6 | additionalPrinterColumns: 7 | - JSONPath: .metadata.creationTimestamp 8 | description: |- 9 | CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. 10 | 11 | Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 12 | name: Age 13 | type: date 14 | group: jenkins.io 15 | names: 16 | kind: SSO 17 | listKind: SSOList 18 | plural: ssos 19 | shortNames: 20 | - sso 21 | singular: sso 22 | scope: Namespaced 23 | version: v1 24 | versions: 25 | - name: v1 26 | served: true 27 | storage: true -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/jenkins.io/v1/interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | internalinterfaces "github.com/jenkins-x/sso-operator/pkg/client/informers/externalversions/internalinterfaces" 7 | ) 8 | 9 | // Interface provides access to all the informers in this group version. 10 | type Interface interface { 11 | // SSOs returns a SSOInformer. 12 | SSOs() SSOInformer 13 | } 14 | 15 | type version struct { 16 | factory internalinterfaces.SharedInformerFactory 17 | namespace string 18 | tweakListOptions internalinterfaces.TweakListOptionsFunc 19 | } 20 | 21 | // New returns a new Interface. 22 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 23 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 24 | } 25 | 26 | // SSOs returns a SSOInformer. 27 | func (v *version) SSOs() SSOInformer { 28 | return &sSOInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 29 | } 30 | -------------------------------------------------------------------------------- /charts/sso-operator/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for Go projects. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | replicaCount: 1 5 | image: 6 | repo: gcr.io/jenkinsxio/sso-operator 7 | tag: 1.2.19 8 | pullPolicy: Always 9 | service: 10 | name: sso-operator 11 | type: ClusterIP 12 | externalPort: 80 13 | internalPort: 8080 14 | resources: 15 | limits: 16 | cpu: 100m 17 | memory: 256Mi 18 | requests: 19 | cpu: 80m 20 | memory: 128Mi 21 | probePath: / 22 | livenessProbe: 23 | initialDelaySeconds: 60 24 | periodSeconds: 10 25 | successThreshold: 1 26 | timeoutSeconds: 1 27 | readinessProbe: 28 | periodSeconds: 10 29 | successThreshold: 1 30 | timeoutSeconds: 1 31 | terminationGracePeriodSeconds: 10 32 | 33 | watch: 34 | namespace: "" # if the namespace is empty, it will watch the entire cluster 35 | 36 | certs: 37 | legacyApi: false 38 | 39 | dex: 40 | grpcHost: dex.sso 41 | grpcPort: 5000 42 | certs: 43 | grpc: 44 | issuer: 45 | name: dex-grpc-cert-issuer 46 | kind: Issuer 47 | client: 48 | secretName: dex-grpc-client-cert 49 | -------------------------------------------------------------------------------- /charts/sso-operator/Makefile: -------------------------------------------------------------------------------- 1 | CHART_REPO := http://jenkins-x-chartmuseum:8080 2 | CURRENT=$(pwd) 3 | NAME := sso-operator 4 | OS := $(shell uname) 5 | 6 | init: 7 | helm init --client-only 8 | 9 | setup: init 10 | helm repo add jenkinsx http://chartmuseum.jenkins-x.io 11 | 12 | build: clean setup 13 | helm dependency build 14 | helm lint . 15 | 16 | install: clean setup build 17 | helm install . --name ${NAME} 18 | 19 | upgrade: clean setup build 20 | helm upgrade ${NAME} . 21 | 22 | delete: 23 | helm delete --purge ${NAME} 24 | 25 | clean: 26 | rm -rf charts 27 | rm -rf ${NAME}*.tgz 28 | rm -rf requirements.lock 29 | 30 | release: clean build 31 | ifeq ($(OS),Darwin) 32 | sed -i "" -e "s/version:.*/version: $(VERSION)/" Chart.yaml 33 | sed -i "" -e "s/tag:.*/tag: $(VERSION)/" values.yaml 34 | 35 | else ifeq ($(OS),Linux) 36 | sed -i -e "s/version:.*/version: $(VERSION)/" Chart.yaml 37 | sed -i -e "s/tag:.*/tag: $(VERSION)/" values.yaml 38 | else 39 | exit -1 40 | endif 41 | helm package . 42 | curl --fail -u $(CHARTMUSEUM_USER):$(CHARTMUSEUM_PASS) --data-binary "@$(NAME)-$(VERSION).tgz" $(CHART_REPO)/api/charts 43 | helm repo update 44 | rm -rf ${NAME}*.tgz 45 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/jenkins.io/interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package jenkins 4 | 5 | import ( 6 | internalinterfaces "github.com/jenkins-x/sso-operator/pkg/client/informers/externalversions/internalinterfaces" 7 | v1 "github.com/jenkins-x/sso-operator/pkg/client/informers/externalversions/jenkins.io/v1" 8 | ) 9 | 10 | // Interface provides access to each of this group's versions. 11 | type Interface interface { 12 | // V1 provides access to shared informers for resources in V1. 13 | V1() v1.Interface 14 | } 15 | 16 | type group struct { 17 | factory internalinterfaces.SharedInformerFactory 18 | namespace string 19 | tweakListOptions internalinterfaces.TweakListOptionsFunc 20 | } 21 | 22 | // New returns a new Interface. 23 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 24 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 25 | } 26 | 27 | // V1 returns a new v1.Interface. 28 | func (g *group) V1() v1.Interface { 29 | return v1.New(g.factory, g.namespace, g.tweakListOptions) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | jenkinsv1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 11 | ) 12 | 13 | var scheme = runtime.NewScheme() 14 | var codecs = serializer.NewCodecFactory(scheme) 15 | var parameterCodec = runtime.NewParameterCodec(scheme) 16 | 17 | func init() { 18 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 19 | AddToScheme(scheme) 20 | } 21 | 22 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 23 | // of clientsets, like in: 24 | // 25 | // import ( 26 | // "k8s.io/client-go/kubernetes" 27 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 28 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 29 | // ) 30 | // 31 | // kclientset, _ := kubernetes.NewForConfig(c) 32 | // aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 33 | // 34 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 35 | // correctly. 36 | func AddToScheme(scheme *runtime.Scheme) { 37 | jenkinsv1.AddToScheme(scheme) // #nosec 38 | } 39 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package scheme 4 | 5 | import ( 6 | jenkinsv1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 11 | ) 12 | 13 | var Scheme = runtime.NewScheme() 14 | var Codecs = serializer.NewCodecFactory(Scheme) 15 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 16 | 17 | func init() { 18 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 19 | AddToScheme(Scheme) 20 | } 21 | 22 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 23 | // of clientsets, like in: 24 | // 25 | // import ( 26 | // "k8s.io/client-go/kubernetes" 27 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 28 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 29 | // ) 30 | // 31 | // kclientset, _ := kubernetes.NewForConfig(c) 32 | // aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 33 | // 34 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 35 | // correctly. 36 | func AddToScheme(scheme *runtime.Scheme) { 37 | jenkinsv1.AddToScheme(scheme) // #nosec 38 | } 39 | -------------------------------------------------------------------------------- /hack/verify-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. 22 | 23 | DIFFROOT="${SCRIPT_ROOT}/pkg" 24 | TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/pkg" 25 | _tmp="${SCRIPT_ROOT}/_tmp" 26 | 27 | cleanup() { 28 | rm -rf "${_tmp}" 29 | } 30 | trap "cleanup" EXIT SIGINT 31 | 32 | cleanup 33 | 34 | mkdir -p "${TMP_DIFFROOT}" 35 | cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}" 36 | 37 | "${SCRIPT_ROOT}/hack/update-codegen.sh" 38 | echo "diffing ${DIFFROOT} against freshly generated codegen" 39 | ret=0 40 | diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? 41 | cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}" 42 | if [[ $ret -eq 0 ]] 43 | then 44 | echo "${DIFFROOT} up to date." 45 | else 46 | echo "${DIFFROOT} is out of date. Please run hack/update-codegen.sh" 47 | exit 1 48 | fi 49 | -------------------------------------------------------------------------------- /pkg/kubernetes/crds.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | jenkinsio "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io" 5 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 6 | apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | // RegisterSSOCRD ensures that the CRD is registered for SSO 11 | func RegisterSSOCRD(apiClient apiextensionsclientset.Interface) error { 12 | name := "ssos." + jenkinsio.GroupName 13 | names := &v1beta1.CustomResourceDefinitionNames{ 14 | Kind: "SSO", 15 | ListKind: "SSOList", 16 | Plural: "ssos", 17 | Singular: "sso", 18 | ShortNames: []string{"sso"}, 19 | } 20 | 21 | return registerCRD(apiClient, name, names) 22 | } 23 | 24 | func registerCRD(apiClient apiextensionsclientset.Interface, name string, names *v1beta1.CustomResourceDefinitionNames) error { 25 | _, err := apiClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{}) 26 | if err == nil { 27 | return nil 28 | } 29 | 30 | crd := &v1beta1.CustomResourceDefinition{ 31 | ObjectMeta: metav1.ObjectMeta{ 32 | Name: name, 33 | }, 34 | Spec: v1beta1.CustomResourceDefinitionSpec{ 35 | Group: jenkinsio.GroupName, 36 | Version: jenkinsio.Version, 37 | Scope: v1beta1.NamespaceScoped, 38 | Names: *names, 39 | }, 40 | } 41 | _, err = apiClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd) 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | NAME := sso-operator 3 | OS := $(shell uname) 4 | MAIN_GO := main.go 5 | GO := GO111MODULE=on go 6 | GO_NOMOD :=GO111MODULE=off go 7 | BUILDFLAGS := '' 8 | CGO_ENABLED = 0 9 | GOPATH ?= $(shell $(GO) env GOPATH) 10 | GOBIN ?= $(GOPATH)/bin 11 | GOLINT ?= $(GOBIN)/golint 12 | GOSEC ?= $(GOBIN)/gosec 13 | 14 | all: fmt lint sec test build 15 | 16 | build: 17 | CGO_ENABLED=$(CGO_ENABLED) $(GO) build -ldflags $(BUILDFLAGS) -o bin/$(NAME) $(MAIN_GO) 18 | 19 | test: 20 | CGO_ENABLED=$(CGO_ENABLED) $(GO) test -test.v ./... 21 | 22 | install: 23 | GOBIN=${GOPATH}/bin $(GO) install -ldflags $(BUILDFLAGS) $(MAIN_GO) 24 | 25 | fmt: 26 | @echo "FORMATTING" 27 | @FORMATTED=`$(GO) fmt ./...` 28 | @([[ ! -z "$(FORMATTED)" ]] && printf "Fixed unformatted files:\n$(FORMATTED)") || true 29 | 30 | clean: 31 | rm -rf build release 32 | 33 | lint: 34 | @echo "LINTING" 35 | $(GO_NOMOD) get golang.org/x/lint/golint 36 | $(GOLINT) -set_exit_status ./... 37 | @echo "VETTING" 38 | $(GO) vet ./... 39 | 40 | sec: 41 | @echo "SECURITY SCANNING" 42 | $(GO_NOMOD) get github.com/securego/gosec/cmd/gosec 43 | $(GOSEC) ./... 44 | 45 | test-coverage: 46 | go test -race -coverprofile=coverage.txt -covermode=atomic 47 | 48 | codegen: 49 | @echo "GENERATING KUBERNETES CRDs" 50 | hack/update-codegen.sh 51 | 52 | linux: 53 | CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=amd64 $(GO) build -ldflags $(BUILDFLAGS) -o bin/$(NAME) $(MAIN_GO) 54 | 55 | install-helm: linux 56 | skaffold run -p install -n $(KUBERNETES_NAMESPACE) 57 | -------------------------------------------------------------------------------- /pkg/apis/jenkins.io/v1/register.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | 7 | jenkinsio "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io" 8 | sdkK8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | var ( 13 | // SchemeBuilder for building the schema 14 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 15 | // AddToScheme helper 16 | AddToScheme = SchemeBuilder.AddToScheme 17 | 18 | // SchemeGroupVersion is group version used to register these objects 19 | SchemeGroupVersion = schema.GroupVersion{Group: jenkinsio.GroupName, Version: jenkinsio.Version} 20 | 21 | // SSOKind is the SSO CRD kind 22 | SSOKind = "SSO" 23 | ) 24 | 25 | func init() { 26 | sdkK8sutil.AddToSDKScheme(AddToScheme) 27 | } 28 | 29 | // Kind takes an unqualified kind and returns back a Group qualified GroupKind 30 | func Kind(kind string) schema.GroupKind { 31 | return SchemeGroupVersion.WithKind(kind).GroupKind() 32 | } 33 | 34 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 35 | func Resource(resource string) schema.GroupResource { 36 | return SchemeGroupVersion.WithResource(resource).GroupResource() 37 | } 38 | 39 | // Adds the list of known types to Scheme. 40 | func addKnownTypes(scheme *runtime.Scheme) error { 41 | scheme.AddKnownTypes(SchemeGroupVersion, 42 | &SSO{}, 43 | &SSOList{}, 44 | ) 45 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /jenkins-x-whitesource.yml: -------------------------------------------------------------------------------- 1 | buildPack: none 2 | noReleasePrepare: true 3 | pipelineConfig: 4 | pipelines: 5 | release: 6 | pipeline: 7 | agent: 8 | image: cloudbees/whitesource-agent-jx 9 | stages: 10 | - name: dependencies-scanning 11 | environment: 12 | - name: WHITESOURCE_API_KEY 13 | valueFrom: 14 | secretKeyRef: 15 | name: whitesource 16 | key: api-key 17 | - name: WHITESOURCE_USER_KEY 18 | valueFrom: 19 | secretKeyRef: 20 | name: whitesource 21 | key: user-key 22 | - name: WHITESOURCE_PRODUCT_TOKEN 23 | value: "499f35ebde4642b2ac550b9a331c2bb86c2deb1bcd5a4b9282f6879a0f2ab225" 24 | - name: WHITESOURCE_PROJECT_TOKEN 25 | value: "81dcad08c0554b088ddfbf50c7c397aa013551cdeb564b42869e0917934f4d4c" 26 | options: 27 | containerOptions: 28 | resources: 29 | limits: 30 | cpu: 2 31 | memory: 2Gi 32 | requests: 33 | cpu: 1 34 | memory: 1Gi 35 | steps: 36 | - name: whitesource 37 | image: cloudbees/whitesource-agent-jx 38 | command: /app/entrypoint-jx.sh 39 | args: 40 | - -c 41 | - /workspace/source/.whitesource.config 42 | - regular 43 | dir: /app/ 44 | -------------------------------------------------------------------------------- /hack/update-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/.. 22 | CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${SCRIPT_ROOT}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} 23 | 24 | OUTDIR=$(dirname ${BASH_SOURCE})/../../../.. 25 | 26 | echo "Generating code to ${OUTDIR}" 27 | 28 | # gene 29 | # rate the code with: 30 | # --output-base because this script should also be able to run inside the vendor dir of 31 | # k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir 32 | # instead of the $GOPATH directly. For normal projects this can be dropped. 33 | ${CODEGEN_PKG}/generate-groups.sh all \ 34 | github.com/jenkins-x/sso-operator/pkg/client github.com/jenkins-x/sso-operator/pkg/apis \ 35 | jenkins.io:v1 \ 36 | --output-base "${OUTDIR}" \ 37 | --go-header-file ${SCRIPT_ROOT}/hack/custom-boilerplate.go.txt 38 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package externalversions 4 | 5 | import ( 6 | "fmt" 7 | 8 | v1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | cache "k8s.io/client-go/tools/cache" 11 | ) 12 | 13 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 14 | // sharedInformers based on type 15 | type GenericInformer interface { 16 | Informer() cache.SharedIndexInformer 17 | Lister() cache.GenericLister 18 | } 19 | 20 | type genericInformer struct { 21 | informer cache.SharedIndexInformer 22 | resource schema.GroupResource 23 | } 24 | 25 | // Informer returns the SharedIndexInformer. 26 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 27 | return f.informer 28 | } 29 | 30 | // Lister returns the GenericLister. 31 | func (f *genericInformer) Lister() cache.GenericLister { 32 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 33 | } 34 | 35 | // ForResource gives generic access to a shared informer of the matching type 36 | // TODO extend this to unknown resources with a client pool 37 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 38 | switch resource { 39 | // Group=jenkins.io, Version=v1 40 | case v1.SchemeGroupVersion.WithResource("ssos"): 41 | return &genericInformer{resource: resource.GroupResource(), informer: f.Jenkins().V1().SSOs().Informer()}, nil 42 | 43 | } 44 | 45 | return nil, fmt.Errorf("no informer found for %v", resource) 46 | } 47 | -------------------------------------------------------------------------------- /charts/sso-operator/templates/role.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRole 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 7 | rules: 8 | - apiGroups: 9 | - jenkins.io 10 | resources: 11 | - "*" 12 | verbs: 13 | - "*" 14 | - apiGroups: 15 | - apiextensions.k8s.io 16 | resources: 17 | - customresourcedefinitions 18 | verbs: 19 | - "*" 20 | - apiGroups: 21 | - extensions 22 | resources: 23 | - ingresses 24 | verbs: 25 | - get 26 | - list 27 | - watch 28 | - patch 29 | - create 30 | - update 31 | - delete 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - configmaps 36 | - secrets 37 | - services 38 | - pods 39 | verbs: 40 | - get 41 | - list 42 | - create 43 | - update 44 | - delete 45 | - watch 46 | - patch 47 | - apiGroups: 48 | - "" 49 | resources: 50 | - serviceaccounts 51 | verbs: 52 | - get 53 | - create 54 | - apiGroups: 55 | - extensions 56 | - apps 57 | resources: 58 | - deployments 59 | verbs: 60 | - get 61 | - list 62 | - create 63 | - update 64 | - delete 65 | - watch 66 | - patch 67 | - apiGroups: 68 | - batch 69 | resources: 70 | - jobs 71 | verbs: 72 | - get 73 | - list 74 | - create 75 | - update 76 | - delete 77 | - apiGroups: 78 | - "" 79 | - "route.openshift.io" 80 | resources: 81 | - routes 82 | verbs: 83 | - get 84 | - list 85 | - watch 86 | - patch 87 | - create 88 | - update 89 | - delete 90 | - apiGroups: 91 | - rbac.authorization.k8s.io 92 | resources: 93 | - clusterroles 94 | - clusterrolebindings 95 | verbs: 96 | - get 97 | - list 98 | - update 99 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | clientset "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned" 7 | jenkinsv1 "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned/typed/jenkins.io/v1" 8 | fakejenkinsv1 "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned/typed/jenkins.io/v1/fake" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/client-go/discovery" 11 | fakediscovery "k8s.io/client-go/discovery/fake" 12 | "k8s.io/client-go/testing" 13 | ) 14 | 15 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 16 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 17 | // without applying any validations and/or defaults. It shouldn't be considered a replacement 18 | // for a real clientset and is mostly useful in simple unit tests. 19 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 20 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 21 | for _, obj := range objects { 22 | if err := o.Add(obj); err != nil { 23 | panic(err) 24 | } 25 | } 26 | 27 | cs := &Clientset{} 28 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 29 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 30 | 31 | return cs 32 | } 33 | 34 | // Clientset implements clientset.Interface. Meant to be embedded into a 35 | // struct to get a default implementation. This makes faking out just the method 36 | // you want to test easier. 37 | type Clientset struct { 38 | testing.Fake 39 | discovery *fakediscovery.FakeDiscovery 40 | } 41 | 42 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 43 | return c.discovery 44 | } 45 | 46 | var _ clientset.Interface = &Clientset{} 47 | 48 | // JenkinsV1 retrieves the JenkinsV1Client 49 | func (c *Clientset) JenkinsV1() jenkinsv1.JenkinsV1Interface { 50 | return &fakejenkinsv1.FakeJenkinsV1{Fake: &c.Fake} 51 | } 52 | 53 | // Jenkins retrieves the JenkinsV1Client 54 | func (c *Clientset) Jenkins() jenkinsv1.JenkinsV1Interface { 55 | return &fakejenkinsv1.FakeJenkinsV1{Fake: &c.Fake} 56 | } 57 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/jenkins.io/v1/jenkins.io_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | v1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 7 | "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned/scheme" 8 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 9 | rest "k8s.io/client-go/rest" 10 | ) 11 | 12 | type JenkinsV1Interface interface { 13 | RESTClient() rest.Interface 14 | SSOsGetter 15 | } 16 | 17 | // JenkinsV1Client is used to interact with features provided by the jenkins.io group. 18 | type JenkinsV1Client struct { 19 | restClient rest.Interface 20 | } 21 | 22 | func (c *JenkinsV1Client) SSOs(namespace string) SSOInterface { 23 | return newSSOs(c, namespace) 24 | } 25 | 26 | // NewForConfig creates a new JenkinsV1Client for the given config. 27 | func NewForConfig(c *rest.Config) (*JenkinsV1Client, error) { 28 | config := *c 29 | if err := setConfigDefaults(&config); err != nil { 30 | return nil, err 31 | } 32 | client, err := rest.RESTClientFor(&config) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &JenkinsV1Client{client}, nil 37 | } 38 | 39 | // NewForConfigOrDie creates a new JenkinsV1Client for the given config and 40 | // panics if there is an error in the config. 41 | func NewForConfigOrDie(c *rest.Config) *JenkinsV1Client { 42 | client, err := NewForConfig(c) 43 | if err != nil { 44 | panic(err) 45 | } 46 | return client 47 | } 48 | 49 | // New creates a new JenkinsV1Client for the given RESTClient. 50 | func New(c rest.Interface) *JenkinsV1Client { 51 | return &JenkinsV1Client{c} 52 | } 53 | 54 | func setConfigDefaults(config *rest.Config) error { 55 | gv := v1.SchemeGroupVersion 56 | config.GroupVersion = &gv 57 | config.APIPath = "/apis" 58 | config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} 59 | 60 | if config.UserAgent == "" { 61 | config.UserAgent = rest.DefaultKubernetesUserAgent() 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // RESTClient returns a RESTClient that is used to communicate 68 | // with API server by this client implementation. 69 | func (c *JenkinsV1Client) RESTClient() rest.Interface { 70 | if c == nil { 71 | return nil 72 | } 73 | return c.restClient 74 | } 75 | -------------------------------------------------------------------------------- /pkg/kubernetes/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 The Skaffold 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 | package kubernetes 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned" 23 | "github.com/pkg/errors" 24 | apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 25 | "k8s.io/client-go/kubernetes" 26 | restclient "k8s.io/client-go/rest" 27 | "k8s.io/client-go/tools/clientcmd" 28 | ) 29 | 30 | // GetClientset creates a new k8s client 31 | func GetClientset() (kubernetes.Interface, error) { 32 | config, err := GetClientConfig() 33 | if err != nil { 34 | return nil, errors.Wrap(err, "getting client config for kubernetes client") 35 | } 36 | return kubernetes.NewForConfig(config) 37 | } 38 | 39 | // GetClientConfig return the k8s configuration 40 | func GetClientConfig() (*restclient.Config, error) { 41 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 42 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) 43 | clientConfig, err := kubeConfig.ClientConfig() 44 | if err != nil { 45 | return nil, fmt.Errorf("Error creating kubeConfig: %s", err) 46 | } 47 | return clientConfig, nil 48 | } 49 | 50 | // GetAPIExtensionsClient returns the k8s api extensions client 51 | func GetAPIExtensionsClient() (apiextensionsclientset.Interface, error) { 52 | config, err := GetClientConfig() 53 | if err != nil { 54 | return nil, errors.Wrap(err, "getting client config for api extensions client") 55 | } 56 | 57 | return apiextensionsclientset.NewForConfig(config) 58 | } 59 | 60 | // GetJenkinsClient returns the Jenkins CRDs client 61 | func GetJenkinsClient() (versioned.Interface, error) { 62 | config, err := GetClientConfig() 63 | if err != nil { 64 | return nil, errors.Wrap(err, "getting client config for jenkins client") 65 | } 66 | 67 | return versioned.NewForConfig(config) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/operator/config.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "github.com/jenkins-x/sso-operator/pkg/kubernetes" 5 | "github.com/pkg/errors" 6 | v1 "k8s.io/api/core/v1" 7 | apierrors "k8s.io/apimachinery/pkg/api/errors" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | const ( 12 | operatorSecretName = "operator-secret" 13 | ssoCookieKey = "ssoCookieKey" 14 | ) 15 | 16 | type operatorConfig struct { 17 | ssoCookieKey string 18 | } 19 | 20 | func getOperatorConfigFromSecret(namespace string) (*operatorConfig, error) { 21 | k8sClient, err := kubernetes.GetClientset() 22 | if err != nil { 23 | return nil, errors.Wrap(err, "getting k8s client") 24 | } 25 | 26 | secret, err := k8sClient.CoreV1().Secrets(namespace).Get(operatorSecretName, metav1.GetOptions{}) 27 | if err != nil { 28 | if apierrors.IsNotFound(err) { 29 | return nil, errors.New("operator config secret not found") 30 | } 31 | delerr := deleteOperatorConfigSecret(namespace) 32 | if delerr != nil { 33 | return nil, errors.Wrap(delerr, "cleaning up the operator config secert") 34 | } 35 | return nil, errors.Wrap(err, "cleaning up the operator config secret due to error") 36 | } 37 | 38 | cookieKey, ok := secret.Data[ssoCookieKey] 39 | if !ok { 40 | delerr := deleteOperatorConfigSecret(namespace) 41 | if delerr != nil { 42 | return nil, errors.Wrap(delerr, "cleaning up the operator config secert because cookie key is missing") 43 | } 44 | return nil, errors.New("sso cookie key not found in operator secret") 45 | } 46 | 47 | return &operatorConfig{ 48 | ssoCookieKey: string(cookieKey), 49 | }, nil 50 | } 51 | 52 | func deleteOperatorConfigSecret(namespace string) error { 53 | k8sClient, err := kubernetes.GetClientset() 54 | if err != nil { 55 | return errors.Wrap(err, "getting k8s client") 56 | } 57 | return k8sClient.CoreV1().Secrets(namespace).Delete(operatorSecretName, &metav1.DeleteOptions{}) 58 | } 59 | 60 | func storeOperatorConfigInSecret(namespace string, config *operatorConfig) error { 61 | k8sClient, err := kubernetes.GetClientset() 62 | if err != nil { 63 | return errors.Wrap(err, "getting k8s client") 64 | } 65 | 66 | secret := &v1.Secret{ 67 | TypeMeta: metav1.TypeMeta{ 68 | APIVersion: "v1", 69 | Kind: "Secret", 70 | }, 71 | ObjectMeta: metav1.ObjectMeta{ 72 | Name: operatorSecretName, 73 | Namespace: namespace, 74 | }, 75 | StringData: map[string]string{ 76 | ssoCookieKey: config.ssoCookieKey, 77 | }, 78 | Type: v1.SecretTypeOpaque, 79 | } 80 | 81 | _, err = k8sClient.CoreV1().Secrets(namespace).Create(secret) 82 | return err 83 | } 84 | -------------------------------------------------------------------------------- /pkg/client/listers/jenkins.io/v1/sso.go: -------------------------------------------------------------------------------- 1 | // Code generated by lister-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | v1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 7 | "k8s.io/apimachinery/pkg/api/errors" 8 | "k8s.io/apimachinery/pkg/labels" 9 | "k8s.io/client-go/tools/cache" 10 | ) 11 | 12 | // SSOLister helps list SSOs. 13 | type SSOLister interface { 14 | // List lists all SSOs in the indexer. 15 | List(selector labels.Selector) (ret []*v1.SSO, err error) 16 | // SSOs returns an object that can list and get SSOs. 17 | SSOs(namespace string) SSONamespaceLister 18 | SSOListerExpansion 19 | } 20 | 21 | // sSOLister implements the SSOLister interface. 22 | type sSOLister struct { 23 | indexer cache.Indexer 24 | } 25 | 26 | // NewSSOLister returns a new SSOLister. 27 | func NewSSOLister(indexer cache.Indexer) SSOLister { 28 | return &sSOLister{indexer: indexer} 29 | } 30 | 31 | // List lists all SSOs in the indexer. 32 | func (s *sSOLister) List(selector labels.Selector) (ret []*v1.SSO, err error) { 33 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 34 | ret = append(ret, m.(*v1.SSO)) 35 | }) 36 | return ret, err 37 | } 38 | 39 | // SSOs returns an object that can list and get SSOs. 40 | func (s *sSOLister) SSOs(namespace string) SSONamespaceLister { 41 | return sSONamespaceLister{indexer: s.indexer, namespace: namespace} 42 | } 43 | 44 | // SSONamespaceLister helps list and get SSOs. 45 | type SSONamespaceLister interface { 46 | // List lists all SSOs in the indexer for a given namespace. 47 | List(selector labels.Selector) (ret []*v1.SSO, err error) 48 | // Get retrieves the SSO from the indexer for a given namespace and name. 49 | Get(name string) (*v1.SSO, error) 50 | SSONamespaceListerExpansion 51 | } 52 | 53 | // sSONamespaceLister implements the SSONamespaceLister 54 | // interface. 55 | type sSONamespaceLister struct { 56 | indexer cache.Indexer 57 | namespace string 58 | } 59 | 60 | // List lists all SSOs in the indexer for a given namespace. 61 | func (s sSONamespaceLister) List(selector labels.Selector) (ret []*v1.SSO, err error) { 62 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 63 | ret = append(ret, m.(*v1.SSO)) 64 | }) 65 | return ret, err 66 | } 67 | 68 | // Get retrieves the SSO from the indexer for a given namespace and name. 69 | func (s sSONamespaceLister) Get(name string) (*v1.SSO, error) { 70 | obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) 71 | if err != nil { 72 | return nil, err 73 | } 74 | if !exists { 75 | return nil, errors.NewNotFound(v1.Resource("sso"), name) 76 | } 77 | return obj.(*v1.SSO), nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package versioned 4 | 5 | import ( 6 | jenkinsv1 "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned/typed/jenkins.io/v1" 7 | discovery "k8s.io/client-go/discovery" 8 | rest "k8s.io/client-go/rest" 9 | flowcontrol "k8s.io/client-go/util/flowcontrol" 10 | ) 11 | 12 | type Interface interface { 13 | Discovery() discovery.DiscoveryInterface 14 | JenkinsV1() jenkinsv1.JenkinsV1Interface 15 | // Deprecated: please explicitly pick a version if possible. 16 | Jenkins() jenkinsv1.JenkinsV1Interface 17 | } 18 | 19 | // Clientset contains the clients for groups. Each group has exactly one 20 | // version included in a Clientset. 21 | type Clientset struct { 22 | *discovery.DiscoveryClient 23 | jenkinsV1 *jenkinsv1.JenkinsV1Client 24 | } 25 | 26 | // JenkinsV1 retrieves the JenkinsV1Client 27 | func (c *Clientset) JenkinsV1() jenkinsv1.JenkinsV1Interface { 28 | return c.jenkinsV1 29 | } 30 | 31 | // Deprecated: Jenkins retrieves the default version of JenkinsClient. 32 | // Please explicitly pick a version. 33 | func (c *Clientset) Jenkins() jenkinsv1.JenkinsV1Interface { 34 | return c.jenkinsV1 35 | } 36 | 37 | // Discovery retrieves the DiscoveryClient 38 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 39 | if c == nil { 40 | return nil 41 | } 42 | return c.DiscoveryClient 43 | } 44 | 45 | // NewForConfig creates a new Clientset for the given config. 46 | func NewForConfig(c *rest.Config) (*Clientset, error) { 47 | configShallowCopy := *c 48 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 49 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 50 | } 51 | var cs Clientset 52 | var err error 53 | cs.jenkinsV1, err = jenkinsv1.NewForConfig(&configShallowCopy) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &cs, nil 63 | } 64 | 65 | // NewForConfigOrDie creates a new Clientset for the given config and 66 | // panics if there is an error in the config. 67 | func NewForConfigOrDie(c *rest.Config) *Clientset { 68 | var cs Clientset 69 | cs.jenkinsV1 = jenkinsv1.NewForConfigOrDie(c) 70 | 71 | cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) 72 | return &cs 73 | } 74 | 75 | // New creates a new Clientset for the given RESTClient. 76 | func New(c rest.Interface) *Clientset { 77 | var cs Clientset 78 | cs.jenkinsV1 = jenkinsv1.New(c) 79 | 80 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 81 | return &cs 82 | } 83 | -------------------------------------------------------------------------------- /charts/sso-operator/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{ $serviceAccount := include "fullname" . }} 2 | {{ $roleName := include "fullname" . }} 3 | apiVersion: extensions/v1beta1 4 | kind: Deployment 5 | metadata: 6 | name: {{ template "fullname" . }} 7 | labels: 8 | draft: {{ default "draft-app" .Values.draft }} 9 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 10 | spec: 11 | replicas: {{ .Values.replicaCount }} 12 | template: 13 | metadata: 14 | labels: 15 | draft: {{ default "draft-app" .Values.draft }} 16 | app: {{ template "fullname" . }} 17 | {{- if .Values.podAnnotations }} 18 | annotations: 19 | {{ toYaml .Values.podAnnotations | indent 8 }} 20 | {{- end }} 21 | spec: 22 | serviceAccountName: {{ $serviceAccount }} 23 | containers: 24 | - name: {{ .Chart.Name }} 25 | image: "{{ .Values.image.repo }}:{{ .Values.image.tag }}" 26 | imagePullPolicy: {{ .Values.image.pullPolicy }} 27 | command: ["/sso-operator"] 28 | args: 29 | - "--dex-grpc-host-port={{ .Values.dex.grpcHost }}:{{ .Values.dex.grpcPort }}" 30 | - "--dex-grpc-client-crt=/etc/dex/tls/tls.crt" 31 | - "--dex-grpc-client-key=/etc/dex/tls/tls.key" 32 | - "--dex-grpc-client-ca=/etc/dex/tls/ca.crt" 33 | - "--cluster-role-name={{ $roleName }}" 34 | env: 35 | - name: OPERATOR_NAMESPACE 36 | value: {{ .Release.Namespace }} 37 | - name: WATCH_NAMESPACE 38 | value: {{ .Values.watch.namespace }} 39 | volumeMounts: 40 | - name: dex-grpc-client-cert 41 | mountPath: /etc/dex/tls 42 | ports: 43 | - containerPort: {{ .Values.service.internalPort }} 44 | livenessProbe: 45 | httpGet: 46 | path: {{ .Values.probePath }} 47 | port: {{ .Values.service.internalPort }} 48 | initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} 49 | periodSeconds: {{ .Values.livenessProbe.periodSeconds }} 50 | successThreshold: {{ .Values.livenessProbe.successThreshold }} 51 | timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} 52 | readinessProbe: 53 | httpGet: 54 | path: {{ .Values.probePath }} 55 | port: {{ .Values.service.internalPort }} 56 | periodSeconds: {{ .Values.readinessProbe.periodSeconds }} 57 | successThreshold: {{ .Values.readinessProbe.successThreshold }} 58 | timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} 59 | resources: 60 | {{ toYaml .Values.resources | indent 12 }} 61 | volumes: 62 | - name: dex-grpc-client-cert 63 | secret: 64 | defaultMode: 420 65 | secretName: {{ .Values.dex.certs.grpc.client.secretName }} 66 | 67 | terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} 68 | -------------------------------------------------------------------------------- /pkg/kubernetes/rbac.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | v1 "k8s.io/api/core/v1" 8 | rbacv1 "k8s.io/api/rbac/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | const ( 13 | subjectKind = "ServiceAccount" 14 | roleKind = "ClusterRole" 15 | serviceAccountName = "sso-operator-sa" 16 | ) 17 | 18 | // EnsureClusterRoleBinding ensures for the given cluster role name that there is a binding to a service account 19 | // in the given namespace 20 | func EnsureClusterRoleBinding(clusterRoleName string, namespace string) (string, error) { 21 | k8sClient, err := GetClientset() 22 | if err != nil { 23 | return "", errors.Wrap(err, "getting k8s client") 24 | } 25 | 26 | roleBindingList, err := k8sClient.RbacV1().ClusterRoleBindings().List(metav1.ListOptions{}) 27 | if err != nil { 28 | return "", errors.Wrap(err, "listing cluster role bindings") 29 | } 30 | for _, roleBinding := range roleBindingList.Items { 31 | roleRef := roleBinding.RoleRef 32 | if roleRef.Kind == roleKind && roleRef.Name == clusterRoleName { 33 | // Check if there is already a service account assigned to the operator cluster role in the given namespace 34 | for _, subj := range roleBinding.Subjects { 35 | if subj.Kind == subjectKind && subj.Namespace == namespace { 36 | return subj.Name, nil 37 | } 38 | } 39 | 40 | saName, err := CreateServiceAccount(serviceAccountName, namespace) 41 | if err != nil { 42 | return "", errors.Wrap(err, "role binding creating service account") 43 | } 44 | 45 | subj := rbacv1.Subject{ 46 | Kind: subjectKind, 47 | Name: saName, 48 | Namespace: namespace, 49 | } 50 | roleBinding.Subjects = append(roleBinding.Subjects, subj) 51 | 52 | _, err = k8sClient.RbacV1().ClusterRoleBindings().Update(&roleBinding) // #nosec 53 | if err != nil { 54 | return "", errors.Wrapf(err, "adding service account '%s' to cluster role binding '%s'", saName, roleBinding.Name) 55 | } 56 | return saName, nil 57 | } 58 | } 59 | 60 | return "", fmt.Errorf("no cluster role binding found for cluster role '%s', make sure a binding existing when deploying the operator", clusterRoleName) 61 | } 62 | 63 | // CreateServiceAccount creates a new service account in the given namespace and returns the service account name 64 | func CreateServiceAccount(name string, namespace string) (string, error) { 65 | k8sClient, err := GetClientset() 66 | if err != nil { 67 | return "", errors.Wrap(err, "getting k8s client") 68 | } 69 | 70 | sa, err := k8sClient.CoreV1().ServiceAccounts(namespace).Get(name, metav1.GetOptions{}) 71 | // If a service account already exists just re-use it 72 | if err == nil { 73 | return sa.Name, nil 74 | } 75 | 76 | sa = &v1.ServiceAccount{ 77 | TypeMeta: metav1.TypeMeta{ 78 | Kind: subjectKind, 79 | APIVersion: "v1", 80 | }, 81 | ObjectMeta: metav1.ObjectMeta{ 82 | Name: name, 83 | Namespace: namespace, 84 | }, 85 | } 86 | 87 | sa, err = k8sClient.CoreV1().ServiceAccounts(namespace).Create(sa) 88 | if err != nil { 89 | return "", errors.Wrapf(err, "creating service account '%s'", sa) 90 | } 91 | 92 | return sa.Name, nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/jenkins.io/v1/sso.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | time "time" 7 | 8 | jenkinsiov1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 9 | versioned "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned" 10 | internalinterfaces "github.com/jenkins-x/sso-operator/pkg/client/informers/externalversions/internalinterfaces" 11 | v1 "github.com/jenkins-x/sso-operator/pkg/client/listers/jenkins.io/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | runtime "k8s.io/apimachinery/pkg/runtime" 14 | watch "k8s.io/apimachinery/pkg/watch" 15 | cache "k8s.io/client-go/tools/cache" 16 | ) 17 | 18 | // SSOInformer provides access to a shared informer and lister for 19 | // SSOs. 20 | type SSOInformer interface { 21 | Informer() cache.SharedIndexInformer 22 | Lister() v1.SSOLister 23 | } 24 | 25 | type sSOInformer struct { 26 | factory internalinterfaces.SharedInformerFactory 27 | tweakListOptions internalinterfaces.TweakListOptionsFunc 28 | namespace string 29 | } 30 | 31 | // NewSSOInformer constructs a new informer for SSO type. 32 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 33 | // one. This reduces memory footprint and number of connections to the server. 34 | func NewSSOInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 35 | return NewFilteredSSOInformer(client, namespace, resyncPeriod, indexers, nil) 36 | } 37 | 38 | // NewFilteredSSOInformer constructs a new informer for SSO type. 39 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 40 | // one. This reduces memory footprint and number of connections to the server. 41 | func NewFilteredSSOInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 42 | return cache.NewSharedIndexInformer( 43 | &cache.ListWatch{ 44 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 45 | if tweakListOptions != nil { 46 | tweakListOptions(&options) 47 | } 48 | return client.JenkinsV1().SSOs(namespace).List(options) 49 | }, 50 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 51 | if tweakListOptions != nil { 52 | tweakListOptions(&options) 53 | } 54 | return client.JenkinsV1().SSOs(namespace).Watch(options) 55 | }, 56 | }, 57 | &jenkinsiov1.SSO{}, 58 | resyncPeriod, 59 | indexers, 60 | ) 61 | } 62 | 63 | func (f *sSOInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 64 | return NewFilteredSSOInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 65 | } 66 | 67 | func (f *sSOInformer) Informer() cache.SharedIndexInformer { 68 | return f.factory.InformerFor(&jenkinsiov1.SSO{}, f.defaultInformer) 69 | } 70 | 71 | func (f *sSOInformer) Lister() v1.SSOLister { 72 | return v1.NewSSOLister(f.Informer().GetIndexer()) 73 | } 74 | -------------------------------------------------------------------------------- /.secrets.baseline: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": { 3 | "files": "Gopkg.lock", 4 | "lines": null 5 | }, 6 | "generated_at": "2019-07-24T17:24:48Z", 7 | "plugins_used": [ 8 | { 9 | "name": "AWSKeyDetector" 10 | }, 11 | { 12 | "name": "ArtifactoryDetector" 13 | }, 14 | { 15 | "base64_limit": 4.5, 16 | "name": "Base64HighEntropyString" 17 | }, 18 | { 19 | "name": "BasicAuthDetector" 20 | }, 21 | { 22 | "hex_limit": 3, 23 | "name": "HexHighEntropyString" 24 | }, 25 | { 26 | "name": "KeywordDetector" 27 | }, 28 | { 29 | "name": "PrivateKeyDetector" 30 | }, 31 | { 32 | "name": "SlackDetector" 33 | }, 34 | { 35 | "name": "StripeDetector" 36 | } 37 | ], 38 | "results": { 39 | "README.md": [ 40 | { 41 | "hashed_secret": "0c9967f3918994e95ab61396a76a7d10f783c8f7", 42 | "is_secret": false, 43 | "is_verified": false, 44 | "line_number": 37, 45 | "type": "Secret Keyword" 46 | } 47 | ], 48 | "pkg/operator/config.go": [ 49 | { 50 | "hashed_secret": "2978f389a32111504f1c3b39df2123be5c453020", 51 | "is_secret": false, 52 | "is_verified": false, 53 | "line_number": 66, 54 | "type": "Secret Keyword" 55 | } 56 | ], 57 | "pkg/proxy/config.go": [ 58 | { 59 | "hashed_secret": "63bd0ca968c64136056bab0d90edf0d926799a2d", 60 | "is_secret": false, 61 | "is_verified": false, 62 | "line_number": 60, 63 | "type": "Secret Keyword" 64 | }, 65 | { 66 | "hashed_secret": "5f96dbbbccb614d78b9855e519162142ce202ce3", 67 | "is_secret": false, 68 | "is_verified": false, 69 | "line_number": 86, 70 | "type": "Secret Keyword" 71 | } 72 | ], 73 | "pkg/proxy/config_test.go": [ 74 | { 75 | "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", 76 | "is_secret": false, 77 | "is_verified": false, 78 | "line_number": 22, 79 | "type": "Secret Keyword" 80 | } 81 | ], 82 | "pkg/proxy/proxy.go": [ 83 | { 84 | "hashed_secret": "d85855897b3e8e8555d73a4a6c09a96cbd77cbd4", 85 | "is_secret": false, 86 | "is_verified": false, 87 | "line_number": 152, 88 | "type": "Secret Keyword" 89 | }, 90 | { 91 | "hashed_secret": "6a572d4a4b2776091609b806b3696801e877ae02", 92 | "is_secret": false, 93 | "is_verified": false, 94 | "line_number": 242, 95 | "type": "Secret Keyword" 96 | }, 97 | { 98 | "hashed_secret": "a830d1fefaa80e5dac8e6810fa1c0b37e9e48747", 99 | "is_secret": false, 100 | "is_verified": false, 101 | "line_number": 396, 102 | "type": "Secret Keyword" 103 | }, 104 | { 105 | "hashed_secret": "2978f389a32111504f1c3b39df2123be5c453020", 106 | "is_secret": false, 107 | "is_verified": false, 108 | "line_number": 432, 109 | "type": "Secret Keyword" 110 | } 111 | ] 112 | }, 113 | "version": "0.12.5" 114 | } 115 | -------------------------------------------------------------------------------- /pkg/proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestBuildNameResource(t *testing.T) { 9 | // 43 chars resource name 10 | resourceName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd" 11 | // 6 chars suffix 12 | suffix := "suffix" 13 | 14 | expectedName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-suffix" 15 | 16 | name := buildName(resourceName, suffix) 17 | 18 | assert.Equal(t, 50, len(name)) 19 | assert.Equal(t, expectedName, name) 20 | } 21 | 22 | func TestBuildNameResourceNameMoreThan63Chars(t *testing.T) { 23 | // 65 chars resource name 24 | resourceName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-eeeeeeeeee-ffffffffff" 25 | // 6 chars suffix 26 | suffix := "suffix" 27 | 28 | expectedName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-eeeeeeeeee-f-suffix" 29 | 30 | name := buildName(resourceName, suffix) 31 | 32 | assert.Equal(t, 63, len(name)) 33 | assert.Equal(t, expectedName, name) 34 | } 35 | 36 | func TestBuildNameSuffixMoreThan63Chars(t *testing.T) { 37 | // 4 chars name 38 | resourceName := "name" 39 | // 65 chars suffix 40 | suffix := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-eeeeeeeeee-ffffffffff" 41 | 42 | expectedName := "name-aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-eeeeeeeeee-fff" 43 | 44 | name := buildName(resourceName, suffix) 45 | 46 | assert.Equal(t, 63, len(name)) 47 | assert.Equal(t, expectedName, name) 48 | } 49 | 50 | func TestBuildNameResourceNameAndSuffixMoreThan63Chars(t *testing.T) { 51 | // 65 chars resource name 52 | resourceName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-eeeeeeeeee-ffffffffff" 53 | // 65 chars suffix 54 | suffix := "gggggggggg-hhhhhhhhhh-iiiiiiiiii-jjjjjjjjjj-kkkkkkkkkk-llllllllll" 55 | 56 | expectedName := "aaaaaaaaaa-bbbbbbbbbb-ccccccccc-gggggggggg-hhhhhhhhhh-iiiiiiiii" 57 | 58 | name := buildName(resourceName, suffix) 59 | 60 | assert.Equal(t, 63, len(name)) 61 | assert.Equal(t, expectedName, name) 62 | } 63 | 64 | func TestBuildNameTrailingDash(t *testing.T) { 65 | // 61 chars resource name 66 | resourceName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-eeeeeeeeee-ffffff" 67 | // 7 chars suffix 68 | suffix := "sufffix" 69 | 70 | expectedName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-eeeeeeeeee-sufffix" 71 | 72 | name := buildName(resourceName, suffix) 73 | 74 | assert.Equal(t, 62, len(name)) 75 | assert.Equal(t, expectedName, name) 76 | } 77 | 78 | func TestBuildNameNoSuffix(t *testing.T) { 79 | // 43 chars resource name 80 | resourceName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd" 81 | // Empty string suffix 82 | suffix := "" 83 | 84 | expectedName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd" 85 | 86 | name := buildName(resourceName, suffix) 87 | 88 | assert.Equal(t, 43, len(name)) 89 | assert.Equal(t, expectedName, name) 90 | } 91 | 92 | func TestBuildNameNoSuffixLongResourceName(t *testing.T) { 93 | // 65 chars resource name 94 | resourceName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-eeeeeeeeee-ffffffffff" 95 | // Empty string suffix 96 | suffix := "" 97 | 98 | expectedName := "aaaaaaaaaa-bbbbbbbbbb-cccccccccc-dddddddddd-eeeeeeeeee-fffffff" 99 | 100 | name := buildName(resourceName, suffix) 101 | 102 | assert.Equal(t, 62, len(name)) 103 | assert.Equal(t, expectedName, name) 104 | } 105 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jenkins-x/sso-operator 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dexidp/dex v0.0.0-20200512115545-709d4169d646 7 | github.com/operator-framework/operator-sdk v0.0.6-0.20180730221907-1c3780f1afb2 8 | github.com/pkg/errors v0.8.1 9 | github.com/sirupsen/logrus v1.8.3 10 | github.com/spf13/cobra v0.0.5 11 | github.com/stretchr/testify v1.7.0 12 | google.golang.org/grpc v1.29.1 13 | gopkg.in/yaml.v2 v2.2.5 14 | k8s.io/api v0.0.0-20180711052118-183f3326a935 15 | k8s.io/apiextensions-apiserver v0.0.0-20180621085152-bbc52469f98b 16 | k8s.io/apimachinery v0.0.0-20180126010752-19e3f5aa3adc 17 | k8s.io/client-go v6.0.1-0.20180103015815-9389c055a838+incompatible 18 | ) 19 | 20 | require ( 21 | github.com/PuerkitoBio/purell v1.1.0 // indirect 22 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 23 | github.com/beorn7/perks v1.0.1 // indirect 24 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/emicklei/go-restful v2.8.0+incompatible // indirect 27 | github.com/ghodss/yaml v1.0.0 // indirect 28 | github.com/go-openapi/jsonpointer v0.0.0-20180322222829-3a0015ad55fa // indirect 29 | github.com/go-openapi/jsonreference v0.0.0-20180322222742-3fb327e6747d // indirect 30 | github.com/go-openapi/spec v0.0.0-20180710175419-bce47c9386f9 // indirect 31 | github.com/go-openapi/swag v0.0.0-20180703152219-2b0bd4f193d0 // indirect 32 | github.com/gogo/protobuf v1.2.1 // indirect 33 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 34 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 35 | github.com/golang/protobuf v1.4.1 // indirect 36 | github.com/google/btree v1.0.0 // indirect 37 | github.com/google/gofuzz v1.0.0 // indirect 38 | github.com/googleapis/gnostic v0.2.0 // indirect 39 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect 40 | github.com/hashicorp/golang-lru v0.5.1 // indirect 41 | github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c // indirect 42 | github.com/imdario/mergo v0.3.6 // indirect 43 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 44 | github.com/json-iterator/go v1.1.9 // indirect 45 | github.com/juju/ratelimit v1.0.1 // indirect 46 | github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5 // indirect 47 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 49 | github.com/modern-go/reflect2 v1.0.1 // indirect 50 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 51 | github.com/pmezard/go-difflib v1.0.0 // indirect 52 | github.com/prometheus/client_golang v1.4.0 // indirect 53 | github.com/prometheus/client_model v0.2.0 // indirect 54 | github.com/prometheus/common v0.9.1 // indirect 55 | github.com/prometheus/procfs v0.0.8 // indirect 56 | github.com/spf13/pflag v1.0.3 // indirect 57 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect 58 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f // indirect 59 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 60 | golang.org/x/text v0.3.2 // indirect 61 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884 // indirect 62 | google.golang.org/protobuf v1.22.0 // indirect 63 | gopkg.in/inf.v0 v0.9.1 // indirect 64 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 65 | k8s.io/kube-openapi v0.0.0-20180719232738-d8ea2fe547a4 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /pkg/proxy/config.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "text/template" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // Cookie holds the configuration for oauth2_proxy cookie 11 | type Cookie struct { 12 | Name string 13 | Secret string 14 | Domain string 15 | Expire string 16 | Refresh string 17 | Secure bool 18 | HTTPOnly bool 19 | } 20 | 21 | // Config holds the configuration for oauth2_proxy 22 | type Config struct { 23 | Port int32 24 | 25 | ClientID string 26 | ClientSecret string 27 | 28 | OIDCIssuerURL string 29 | RedirectURL string 30 | LoginURL string 31 | RedeemURL string 32 | 33 | Upstream string 34 | ForwardToken bool 35 | 36 | Cookie Cookie 37 | } 38 | 39 | const proxyConfigTemplate = ` 40 | # OAuth2 Proxy Config File 41 | 42 | ## : to listen on for HTTP/HTTPS clients 43 | http_address = ":{{.Port}}" 44 | 45 | ## the OAuth URLs. 46 | redirect_url = "{{.RedirectURL}}" 47 | login_url = "{{.LoginURL}}" 48 | redeem_url = "{{.RedeemURL}}" 49 | 50 | ## the http url(s) of the upstream endpoint. If multiple, routing is based on path 51 | upstreams = [ 52 | "{{.Upstream}}" 53 | ] 54 | 55 | ## Log requests to stdout 56 | request_logging = true 57 | 58 | ## The OAuth Client ID, Secret 59 | client_id = "{{.ClientID}}" 60 | client_secret = "{{.ClientSecret}}" 61 | 62 | ## Pass OAuth Access token to upstream via "X-Forwarded-Access-Token" 63 | pass_basic_auth = false 64 | pass_host_header = false 65 | pass_access_token = {{.ForwardToken}} 66 | 67 | ## Email Domains to allow authentication for (this authorizes any email on this domain) 68 | email_domains = [ 69 | "*" 70 | ] 71 | 72 | ## Cookie Settings 73 | ## Name - the cookie name 74 | ## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes 75 | ## for use with an AES cipher when cookie_refresh or pass_access_token 76 | ## is set 77 | ## Domain - (optional) cookie domain to force cookies to (ie: .yourcompany.com) 78 | ## Expire - (duration) expire timeframe for cookie 79 | ## Refresh - (duration) refresh the cookie when duration has elapsed after cookie was initially set. 80 | ## Should be less than cookie_expire; set to 0 to disable. 81 | ## On refresh, OAuth token is re-validated. 82 | ## (ie: 1h means tokens are refreshed on request 1hr+ after it was set) 83 | ## Secure - secure cookies are only sent by the browser of a HTTPS connection (recommended) 84 | ## HttpOnly - httponly cookies are not readable by javascript (recommended) 85 | cookie_name = "{{.Cookie.Name}}" 86 | cookie_secret = "{{.Cookie.Secret}}" 87 | cookie_domain = "{{.Cookie.Domain}}" 88 | cookie_expire = "{{.Cookie.Expire}}" 89 | cookie_refresh = "{{.Cookie.Refresh}}" 90 | cookie_secure = {{.Cookie.Secure}} 91 | cookie_httponly = {{.Cookie.HTTPOnly}} 92 | 93 | ## Provider Specific Configurations 94 | provider = "oidc" 95 | oidc_issuer_url = "{{.OIDCIssuerURL}}" 96 | scope = "openid email profile groups federated:id" 97 | skip_provider_button = true 98 | ` 99 | 100 | func renderConfig(config *Config) (string, error) { 101 | tmpl := template.New("oauth2_proxy.tpl") 102 | 103 | tmpl, err := tmpl.Parse(proxyConfigTemplate) 104 | if err != nil { 105 | return "", errors.Wrap(err, "parsing proxy config template") 106 | } 107 | 108 | var buf bytes.Buffer 109 | err = tmpl.Execute(&buf, *config) 110 | if err != nil { 111 | return "", errors.Wrap(err, "rendering proxy config") 112 | } 113 | 114 | return buf.String(), nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/apis/jenkins.io/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "k8s.io/api/core/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 | // +k8s:openapi-gen=true 12 | 13 | // SSO represent Single Sign-On required to create a OIDC client in dex 14 | type SSO struct { 15 | metav1.TypeMeta `json:",inline"` 16 | // Standard object's metadata. 17 | // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 18 | // +optional 19 | metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 20 | 21 | Spec SSOSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` 22 | Status SSOStatus `json:"status,omitempty" protobuf:"bytes,2,opt,name=status"` 23 | } 24 | 25 | // SSOSpec is the specification of a Single Sing-On resource 26 | type SSOSpec struct { 27 | // OIDCIssuerURL URL of dex IdP 28 | OIDCIssuerURL string `json:"oidcIssuerUrl,omitempty"` 29 | // Name of the upstream service for which the SSO is created 30 | UpstreamService string `json:"upstreamService,omitempty"` 31 | // Domain name under which the SSO service will be exposed 32 | Domain string `json:"domain,omitempty"` 33 | // cert-manager issuer name 34 | CertIssuerName string `json:"certIssuerName,omitempty"` 35 | // Docker image for oauth2_proxy 36 | ProxyImage string `json:"proxyImage,omitempty"` 37 | // Docker image tag for oauth2_proxy 38 | ProxyImageTag string `json:"proxyImageTag,omitempty"` 39 | // Docker image PullSecret for oauth2_proxy 40 | ProxyImagePullSecret string `json:"proxyImagePullSecret,omitempty"` 41 | // Resource requirements for oauth2_proxy pod 42 | ProxyResources v1.ResourceRequirements `json:"proxyResources,omitempty"` 43 | // Indicate if the access token should be forwarded to the upstream service 44 | ForwardToken bool `json:"forwardToken,omitempty"` 45 | // CookieSpec cookie specifications 46 | CookieSpec CookieSpec `json:"cookieSpec,omitempty"` 47 | // URLTemplate to use in the exposecontroller configMap 48 | URLTemplate string `json:"urlTemplate,omitempty"` 49 | // SkipExposeService to avoid using exposecontroller to create ingress rule for proxy 50 | SkipExposeService bool `json:"skipExposeService,omitempty"` 51 | // SSLInsecureSkipVerify allows the proxy container to connect with a OIDC using selft-sogned certs, this should be used for testing only 52 | SSLInsecureSkipVerify bool `json:"sslInsecureSkipVerify,omitempty"` 53 | } 54 | 55 | // CookieSpec is the specification of a cookie for a Single Sign-On resource 56 | type CookieSpec struct { 57 | // Cookie name 58 | Name string `json:"name,omitempty"` 59 | // Expiration time of the cookie 60 | Expire string `json:"expire,omitempty"` 61 | // Refresh time of the cookie 62 | Refresh string `json:"refresh,omitempty"` 63 | // Cookie is only send over a HTTPS connection 64 | Secure bool `json:"secure,omitempty"` 65 | // Cookie is not readable from JavaScript 66 | HTTPOnly bool `json:"httpOnly,omitempty"` 67 | } 68 | 69 | // SSOStatus is the status of an Single Sign-On resource 70 | type SSOStatus struct { 71 | // OIDC client ID created in dex 72 | ClientID string `json:"clientId,omitempty" protobuf:"bytes,2,opt,name=clientId"` 73 | // Initialized indicated if the SSO was configured in dex and oauth2_proxy 74 | Initialized bool `json:"initialized,omitempty" protobuf:"bytes,2,opt,name=initialized"` 75 | } 76 | 77 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 78 | 79 | // SSOList represents a list of Single Sign-On Kubernetes objects 80 | type SSOList struct { 81 | metav1.TypeMeta `json:",inline"` 82 | metav1.ListMeta `json:"metadata"` 83 | Items []SSO `json:"items"` 84 | } 85 | -------------------------------------------------------------------------------- /pkg/apis/jenkins.io/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | // Code generated by deepcopy-gen. DO NOT EDIT. 5 | 6 | package v1 7 | 8 | import ( 9 | runtime "k8s.io/apimachinery/pkg/runtime" 10 | ) 11 | 12 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 13 | func (in *CookieSpec) DeepCopyInto(out *CookieSpec) { 14 | *out = *in 15 | return 16 | } 17 | 18 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CookieSpec. 19 | func (in *CookieSpec) DeepCopy() *CookieSpec { 20 | if in == nil { 21 | return nil 22 | } 23 | out := new(CookieSpec) 24 | in.DeepCopyInto(out) 25 | return out 26 | } 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *SSO) DeepCopyInto(out *SSO) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | out.Status = in.Status 35 | return 36 | } 37 | 38 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSO. 39 | func (in *SSO) DeepCopy() *SSO { 40 | if in == nil { 41 | return nil 42 | } 43 | out := new(SSO) 44 | in.DeepCopyInto(out) 45 | return out 46 | } 47 | 48 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 49 | func (in *SSO) DeepCopyObject() runtime.Object { 50 | if c := in.DeepCopy(); c != nil { 51 | return c 52 | } 53 | return nil 54 | } 55 | 56 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 57 | func (in *SSOList) DeepCopyInto(out *SSOList) { 58 | *out = *in 59 | out.TypeMeta = in.TypeMeta 60 | out.ListMeta = in.ListMeta 61 | if in.Items != nil { 62 | in, out := &in.Items, &out.Items 63 | *out = make([]SSO, len(*in)) 64 | for i := range *in { 65 | (*in)[i].DeepCopyInto(&(*out)[i]) 66 | } 67 | } 68 | return 69 | } 70 | 71 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSOList. 72 | func (in *SSOList) DeepCopy() *SSOList { 73 | if in == nil { 74 | return nil 75 | } 76 | out := new(SSOList) 77 | in.DeepCopyInto(out) 78 | return out 79 | } 80 | 81 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 82 | func (in *SSOList) DeepCopyObject() runtime.Object { 83 | if c := in.DeepCopy(); c != nil { 84 | return c 85 | } 86 | return nil 87 | } 88 | 89 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 90 | func (in *SSOSpec) DeepCopyInto(out *SSOSpec) { 91 | *out = *in 92 | in.ProxyResources.DeepCopyInto(&out.ProxyResources) 93 | out.CookieSpec = in.CookieSpec 94 | return 95 | } 96 | 97 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSOSpec. 98 | func (in *SSOSpec) DeepCopy() *SSOSpec { 99 | if in == nil { 100 | return nil 101 | } 102 | out := new(SSOSpec) 103 | in.DeepCopyInto(out) 104 | return out 105 | } 106 | 107 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 108 | func (in *SSOStatus) DeepCopyInto(out *SSOStatus) { 109 | *out = *in 110 | return 111 | } 112 | 113 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSOStatus. 114 | func (in *SSOStatus) DeepCopy() *SSOStatus { 115 | if in == nil { 116 | return nil 117 | } 118 | out := new(SSOStatus) 119 | in.DeepCopyInto(out) 120 | return out 121 | } 122 | -------------------------------------------------------------------------------- /pkg/dex/client.go: -------------------------------------------------------------------------------- 1 | package dex 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "io/ioutil" 9 | 10 | "github.com/dexidp/dex/api" 11 | "github.com/pkg/errors" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials" 14 | ) 15 | 16 | // Options keeps some configuration options for Dex client 17 | type Options struct { 18 | // HostAndPort host name and port of gRPC server 19 | HostAndPort string 20 | // ClientCrt TLS certificate for gRPC client 21 | ClientCrt string 22 | // ClientKey TLS certificate key for gRPC client 23 | ClientKey string 24 | // ClientCA self signed CA certificate for gRPC TLS connection 25 | ClientCA string 26 | } 27 | 28 | // Client represent a client wrapper for Dex 29 | type Client struct { 30 | dex api.DexClient 31 | } 32 | 33 | // NewClient creates a new Dex client 34 | func NewClient(opts *Options) (*Client, error) { 35 | certPool := x509.NewCertPool() 36 | caCert, err := ioutil.ReadFile(opts.ClientCA) // #nosec 37 | if err != nil { 38 | return nil, errors.Wrapf(err, "reading the public CA cert from %q", opts.ClientCA) 39 | } 40 | appended := certPool.AppendCertsFromPEM(caCert) 41 | if !appended { 42 | return nil, errors.New("failed to append the CA cert to the certs pool") 43 | } 44 | 45 | clientCert, err := tls.LoadX509KeyPair(opts.ClientCrt, opts.ClientKey) 46 | if err != nil { 47 | return nil, errors.Wrapf(err, "loading the client cert %q and private key %q", 48 | opts.ClientCrt, opts.ClientKey) 49 | } 50 | 51 | clientTLSConfig := &tls.Config{ 52 | RootCAs: certPool, 53 | Certificates: []tls.Certificate{clientCert}, 54 | MinVersion: tls.VersionTLS12, 55 | } 56 | creds := credentials.NewTLS(clientTLSConfig) 57 | 58 | conn, err := grpc.Dial(opts.HostAndPort, grpc.WithTransportCredentials(creds)) 59 | if err != nil { 60 | return nil, errors.Wrapf(err, "opening the gRPC connection with server %q", opts.HostAndPort) 61 | } 62 | return &Client{ 63 | dex: api.NewDexClient(conn), 64 | }, nil 65 | } 66 | 67 | // CreateClient a new OIDC client in Dex 68 | func (c *Client) CreateClient(ctx context.Context, redirectUris []string, trustedPeers []string, 69 | public bool, name string, logoURL string) (*api.Client, error) { 70 | req := &api.CreateClientReq{ 71 | Client: &api.Client{ 72 | RedirectUris: redirectUris, 73 | TrustedPeers: trustedPeers, 74 | Public: public, 75 | Name: name, 76 | LogoUrl: logoURL, 77 | }, 78 | } 79 | 80 | res, err := c.dex.CreateClient(ctx, req) 81 | if err != nil { 82 | return nil, errors.Wrap(err, "failed to create the OIDC client") 83 | } 84 | 85 | if res.AlreadyExists { 86 | return nil, errors.Wrapf(err, "client %q already exists", res.Client.Id) 87 | } 88 | 89 | return res.Client, nil 90 | } 91 | 92 | // UpdateClient updates an already registered OIDC client 93 | func (c *Client) UpdateClient(ctx context.Context, clientID string, redirectUris []string, 94 | trustedPeers []string, public bool, name string, logoURL string) error { 95 | req := &api.UpdateClientReq{ 96 | Id: clientID, 97 | RedirectUris: redirectUris, 98 | TrustedPeers: trustedPeers, 99 | Name: name, 100 | LogoUrl: logoURL, 101 | } 102 | 103 | res, err := c.dex.UpdateClient(ctx, req) 104 | if err != nil { 105 | return errors.Wrapf(err, "failed to update the client with id %q", clientID) 106 | } 107 | 108 | if res.NotFound { 109 | return fmt.Errorf("update did not find the client with id %q", clientID) 110 | } 111 | return nil 112 | } 113 | 114 | // DeleteClient deletes the client with given Id from Dex 115 | func (c *Client) DeleteClient(ctx context.Context, id string) error { 116 | req := &api.DeleteClientReq{ 117 | Id: id, 118 | } 119 | res, err := c.dex.DeleteClient(ctx, req) 120 | if err != nil { 121 | return errors.Wrapf(err, "failed to delete the client with id %q", id) 122 | } 123 | if res.NotFound { 124 | return fmt.Errorf("delete did not find the client with id %q", id) 125 | } 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/jenkins.io/v1/fake/fake_sso.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | jenkinsiov1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | labels "k8s.io/apimachinery/pkg/labels" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | types "k8s.io/apimachinery/pkg/types" 11 | watch "k8s.io/apimachinery/pkg/watch" 12 | testing "k8s.io/client-go/testing" 13 | ) 14 | 15 | // FakeSSOs implements SSOInterface 16 | type FakeSSOs struct { 17 | Fake *FakeJenkinsV1 18 | ns string 19 | } 20 | 21 | var ssosResource = schema.GroupVersionResource{Group: "jenkins.io", Version: "v1", Resource: "ssos"} 22 | 23 | var ssosKind = schema.GroupVersionKind{Group: "jenkins.io", Version: "v1", Kind: "SSO"} 24 | 25 | // Get takes name of the sSO, and returns the corresponding sSO object, and an error if there is any. 26 | func (c *FakeSSOs) Get(name string, options v1.GetOptions) (result *jenkinsiov1.SSO, err error) { 27 | obj, err := c.Fake. 28 | Invokes(testing.NewGetAction(ssosResource, c.ns, name), &jenkinsiov1.SSO{}) 29 | 30 | if obj == nil { 31 | return nil, err 32 | } 33 | return obj.(*jenkinsiov1.SSO), err 34 | } 35 | 36 | // List takes label and field selectors, and returns the list of SSOs that match those selectors. 37 | func (c *FakeSSOs) List(opts v1.ListOptions) (result *jenkinsiov1.SSOList, err error) { 38 | obj, err := c.Fake. 39 | Invokes(testing.NewListAction(ssosResource, ssosKind, c.ns, opts), &jenkinsiov1.SSOList{}) 40 | 41 | if obj == nil { 42 | return nil, err 43 | } 44 | 45 | label, _, _ := testing.ExtractFromListOptions(opts) 46 | if label == nil { 47 | label = labels.Everything() 48 | } 49 | list := &jenkinsiov1.SSOList{ListMeta: obj.(*jenkinsiov1.SSOList).ListMeta} 50 | for _, item := range obj.(*jenkinsiov1.SSOList).Items { 51 | if label.Matches(labels.Set(item.Labels)) { 52 | list.Items = append(list.Items, item) 53 | } 54 | } 55 | return list, err 56 | } 57 | 58 | // Watch returns a watch.Interface that watches the requested sSOs. 59 | func (c *FakeSSOs) Watch(opts v1.ListOptions) (watch.Interface, error) { 60 | return c.Fake. 61 | InvokesWatch(testing.NewWatchAction(ssosResource, c.ns, opts)) 62 | 63 | } 64 | 65 | // Create takes the representation of a sSO and creates it. Returns the server's representation of the sSO, and an error, if there is any. 66 | func (c *FakeSSOs) Create(sSO *jenkinsiov1.SSO) (result *jenkinsiov1.SSO, err error) { 67 | obj, err := c.Fake. 68 | Invokes(testing.NewCreateAction(ssosResource, c.ns, sSO), &jenkinsiov1.SSO{}) 69 | 70 | if obj == nil { 71 | return nil, err 72 | } 73 | return obj.(*jenkinsiov1.SSO), err 74 | } 75 | 76 | // Update takes the representation of a sSO and updates it. Returns the server's representation of the sSO, and an error, if there is any. 77 | func (c *FakeSSOs) Update(sSO *jenkinsiov1.SSO) (result *jenkinsiov1.SSO, err error) { 78 | obj, err := c.Fake. 79 | Invokes(testing.NewUpdateAction(ssosResource, c.ns, sSO), &jenkinsiov1.SSO{}) 80 | 81 | if obj == nil { 82 | return nil, err 83 | } 84 | return obj.(*jenkinsiov1.SSO), err 85 | } 86 | 87 | // Delete takes name of the sSO and deletes it. Returns an error if one occurs. 88 | func (c *FakeSSOs) Delete(name string, options *v1.DeleteOptions) error { 89 | _, err := c.Fake. 90 | Invokes(testing.NewDeleteAction(ssosResource, c.ns, name), &jenkinsiov1.SSO{}) 91 | 92 | return err 93 | } 94 | 95 | // DeleteCollection deletes a collection of objects. 96 | func (c *FakeSSOs) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { 97 | action := testing.NewDeleteCollectionAction(ssosResource, c.ns, listOptions) 98 | 99 | _, err := c.Fake.Invokes(action, &jenkinsiov1.SSOList{}) 100 | return err 101 | } 102 | 103 | // Patch applies the patch and returns the patched sSO. 104 | func (c *FakeSSOs) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *jenkinsiov1.SSO, err error) { 105 | obj, err := c.Fake. 106 | Invokes(testing.NewPatchSubresourceAction(ssosResource, c.ns, name, data, subresources...), &jenkinsiov1.SSO{}) 107 | 108 | if obj == nil { 109 | return nil, err 110 | } 111 | return obj.(*jenkinsiov1.SSO), err 112 | } 113 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/jenkins.io/v1/sso.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | v1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 7 | scheme "github.com/jenkins-x/sso-operator/pkg/client/clientset/versioned/scheme" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | types "k8s.io/apimachinery/pkg/types" 10 | watch "k8s.io/apimachinery/pkg/watch" 11 | rest "k8s.io/client-go/rest" 12 | ) 13 | 14 | // SSOsGetter has a method to return a SSOInterface. 15 | // A group's client should implement this interface. 16 | type SSOsGetter interface { 17 | SSOs(namespace string) SSOInterface 18 | } 19 | 20 | // SSOInterface has methods to work with SSO resources. 21 | type SSOInterface interface { 22 | Create(*v1.SSO) (*v1.SSO, error) 23 | Update(*v1.SSO) (*v1.SSO, error) 24 | Delete(name string, options *metav1.DeleteOptions) error 25 | DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error 26 | Get(name string, options metav1.GetOptions) (*v1.SSO, error) 27 | List(opts metav1.ListOptions) (*v1.SSOList, error) 28 | Watch(opts metav1.ListOptions) (watch.Interface, error) 29 | Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.SSO, err error) 30 | SSOExpansion 31 | } 32 | 33 | // sSOs implements SSOInterface 34 | type sSOs struct { 35 | client rest.Interface 36 | ns string 37 | } 38 | 39 | // newSSOs returns a SSOs 40 | func newSSOs(c *JenkinsV1Client, namespace string) *sSOs { 41 | return &sSOs{ 42 | client: c.RESTClient(), 43 | ns: namespace, 44 | } 45 | } 46 | 47 | // Get takes name of the sSO, and returns the corresponding sSO object, and an error if there is any. 48 | func (c *sSOs) Get(name string, options metav1.GetOptions) (result *v1.SSO, err error) { 49 | result = &v1.SSO{} 50 | err = c.client.Get(). 51 | Namespace(c.ns). 52 | Resource("ssos"). 53 | Name(name). 54 | VersionedParams(&options, scheme.ParameterCodec). 55 | Do(). 56 | Into(result) 57 | return 58 | } 59 | 60 | // List takes label and field selectors, and returns the list of SSOs that match those selectors. 61 | func (c *sSOs) List(opts metav1.ListOptions) (result *v1.SSOList, err error) { 62 | result = &v1.SSOList{} 63 | err = c.client.Get(). 64 | Namespace(c.ns). 65 | Resource("ssos"). 66 | VersionedParams(&opts, scheme.ParameterCodec). 67 | Do(). 68 | Into(result) 69 | return 70 | } 71 | 72 | // Watch returns a watch.Interface that watches the requested sSOs. 73 | func (c *sSOs) Watch(opts metav1.ListOptions) (watch.Interface, error) { 74 | opts.Watch = true 75 | return c.client.Get(). 76 | Namespace(c.ns). 77 | Resource("ssos"). 78 | VersionedParams(&opts, scheme.ParameterCodec). 79 | Watch() 80 | } 81 | 82 | // Create takes the representation of a sSO and creates it. Returns the server's representation of the sSO, and an error, if there is any. 83 | func (c *sSOs) Create(sSO *v1.SSO) (result *v1.SSO, err error) { 84 | result = &v1.SSO{} 85 | err = c.client.Post(). 86 | Namespace(c.ns). 87 | Resource("ssos"). 88 | Body(sSO). 89 | Do(). 90 | Into(result) 91 | return 92 | } 93 | 94 | // Update takes the representation of a sSO and updates it. Returns the server's representation of the sSO, and an error, if there is any. 95 | func (c *sSOs) Update(sSO *v1.SSO) (result *v1.SSO, err error) { 96 | result = &v1.SSO{} 97 | err = c.client.Put(). 98 | Namespace(c.ns). 99 | Resource("ssos"). 100 | Name(sSO.Name). 101 | Body(sSO). 102 | Do(). 103 | Into(result) 104 | return 105 | } 106 | 107 | // Delete takes name of the sSO and deletes it. Returns an error if one occurs. 108 | func (c *sSOs) Delete(name string, options *metav1.DeleteOptions) error { 109 | return c.client.Delete(). 110 | Namespace(c.ns). 111 | Resource("ssos"). 112 | Name(name). 113 | Body(options). 114 | Do(). 115 | Error() 116 | } 117 | 118 | // DeleteCollection deletes a collection of objects. 119 | func (c *sSOs) DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error { 120 | return c.client.Delete(). 121 | Namespace(c.ns). 122 | Resource("ssos"). 123 | VersionedParams(&listOptions, scheme.ParameterCodec). 124 | Body(options). 125 | Do(). 126 | Error() 127 | } 128 | 129 | // Patch applies the patch and returns the patched sSO. 130 | func (c *sSOs) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.SSO, err error) { 131 | result = &v1.SSO{} 132 | err = c.client.Patch(pt). 133 | Namespace(c.ns). 134 | Resource("ssos"). 135 | SubResource(subresources...). 136 | Name(name). 137 | Body(data). 138 | Do(). 139 | Into(result) 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /jenkins-x.yml: -------------------------------------------------------------------------------- 1 | pipelineConfig: 2 | pipelines: 3 | release: 4 | pipeline: 5 | agent: 6 | image: gcr.io/kaniko-project/executor:9912ccbf8d22bbafbf971124600fbb0b13b9cbd6 7 | stages: 8 | - name: release 9 | environment: 10 | - name: GIT_COMMITTER_EMAIL 11 | value: jenkins-x@googlegroups.com 12 | - name: GIT_AUTHOR_EMAIL 13 | value: jenkins-x@googlegroups.com 14 | - name: GIT_AUTHOR_NAME 15 | value: jenkins-x-bot 16 | - name: GIT_COMMITTER_NAME 17 | value: jenkins-x-bot 18 | - name: BASE_WORKSPACE 19 | value: /workspace/source 20 | - name: HELM_HOME 21 | value: /builder/home/.helm 22 | - name: GOOGLE_APPLICATION_CREDENTIALS 23 | value: /secrets/kaniko/kaniko-secret.json 24 | - name: GOPROXY 25 | value: http://jx-app-athens-athens-proxy 26 | - name: APP_NAME 27 | value: sso-operator 28 | - name: CHARTMUSEUM_USER 29 | valueFrom: 30 | secretKeyRef: 31 | name: jenkins-x-chartmuseum 32 | key: BASIC_AUTH_USER 33 | - name: CHARTMUSEUM_PASS 34 | valueFrom: 35 | secretKeyRef: 36 | name: jenkins-x-chartmuseum 37 | key: BASIC_AUTH_PASS 38 | options: 39 | volumes: 40 | - name: kaniko-secret 41 | secret: 42 | secretName: kaniko-secret 43 | items: 44 | - key: kaniko-secret 45 | path: kaniko/kaniko-secret.json 46 | containerOptions: 47 | volumeMounts: 48 | - name: kaniko-secret 49 | mountPath: /secrets 50 | 51 | steps: 52 | - name: build-and-push-image 53 | image: gcr.io/kaniko-project/executor:9912ccbf8d22bbafbf971124600fbb0b13b9cbd6 54 | command: /kaniko/executor 55 | args: 56 | - --dockerfile=/workspace/source/Dockerfile 57 | - --destination=gcr.io/jenkinsxio/sso-operator:${inputs.params.version} 58 | - --destination=gcr.io/jenkinsxio/sso-operator:latest 59 | - --context=/workspace/source 60 | - --cache-dir=/workspace 61 | dir: /workspace/source 62 | - name: release-charts 63 | image: gcr.io/jenkinsxio/builder-go 64 | command: make 65 | args: 66 | - release 67 | dir: /workspace/source/charts/sso-operator 68 | - name: update-chart-version-jx-versions 69 | image: gcr.io/jenkinsxio/builder-go 70 | command: jx 71 | args: 72 | - step 73 | - create 74 | - pr 75 | - chart 76 | - --name 77 | - sso-operator 78 | - --version 79 | - ${inputs.params.version} 80 | - --repo 81 | - https://github.com/jenkins-x/jenkins-x-versions.git 82 | dir: /workspace/source 83 | - name: update-chart-version-sso-app 84 | image: gcr.io/jenkinsxio/builder-go 85 | command: jx 86 | args: 87 | - step 88 | - create 89 | - pr 90 | - chart 91 | - --name 92 | - sso-operator 93 | - --version 94 | - ${inputs.params.version} 95 | - --repo 96 | - https://github.com/jenkins-x-apps/jx-app-sso.git 97 | dir: /workspace/source 98 | 99 | pullRequest: 100 | pipeline: 101 | agent: 102 | image: gcr.io/kaniko-project/executor:9912ccbf8d22bbafbf971124600fbb0b13b9cbd6 103 | stages: 104 | - name: ci 105 | environment: 106 | - name: GIT_COMMITTER_EMAIL 107 | value: jenkins-x@googlegroups.com 108 | - name: GIT_AUTHOR_EMAIL 109 | value: jenkins-x@googlegroups.com 110 | - name: GIT_AUTHOR_NAME 111 | value: jenkins-x-bot 112 | - name: GIT_COMMITTER_NAME 113 | value: jenkins-x-bot 114 | - name: BASE_WORKSPACE 115 | value: /workspace/source 116 | - name: GOPROXY 117 | value: http://jenkins-x-athens-proxy:80 118 | - name: GOOGLE_APPLICATION_CREDENTIALS 119 | value: /secrets/kaniko/kaniko-secret.json 120 | - name: APP_NAME 121 | value: sso-operator 122 | options: 123 | volumes: 124 | - name: kaniko-secret 125 | secret: 126 | secretName: kaniko-secret 127 | items: 128 | - key: kaniko-secret 129 | path: kaniko/kaniko-secret.json 130 | containerOptions: 131 | volumeMounts: 132 | - name: kaniko-secret 133 | mountPath: /secrets 134 | 135 | steps: 136 | - name: tests 137 | image: gcr.io/jenkinsxio/builder-go 138 | command: make 139 | args: 140 | - all 141 | dir: /workspace/source 142 | - name: build-charts 143 | image: gcr.io/jenkinsxio/builder-go 144 | command: make 145 | args: 146 | - build 147 | dir: /workspace/source/charts/sso-operator 148 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | 11 | "github.com/jenkins-x/sso-operator/pkg/dex" 12 | "github.com/jenkins-x/sso-operator/pkg/kubernetes" 13 | "github.com/jenkins-x/sso-operator/pkg/operator" 14 | sdk "github.com/operator-framework/operator-sdk/pkg/sdk" 15 | sdkVersion "github.com/operator-framework/operator-sdk/version" 16 | 17 | "github.com/sirupsen/logrus" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | const ( 22 | watchNamespaceEnv = "WATCH_NAMESPACE" 23 | operatorNamespaceEnv = "OPERATOR_NAMESPACE" 24 | port = "8080" 25 | ) 26 | 27 | // OperatorOptions holds the command options for SSO operator 28 | type OperatorOptions struct { 29 | Namespace string 30 | WatchNamespace string 31 | DexGrpcHostAndPort string 32 | DexGrpcClientCrt string 33 | DexGrpcClientKey string 34 | DexGrpcClientCA string 35 | ClusterRoleName string 36 | } 37 | 38 | func printVersion(namespace string, watchNamespace string) { 39 | logrus.Infof("Go Version: %s", runtime.Version()) 40 | logrus.Infof("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH) 41 | logrus.Infof("operator-sdk Version: %v", sdkVersion.Version) 42 | logrus.Infof("operator namespace: %s", namespace) 43 | if watchNamespace == "" { 44 | logrus.Info("operator watching entire cluster") 45 | } else { 46 | logrus.Infof("operator watching namespace: %s", watchNamespace) 47 | } 48 | } 49 | 50 | func handleLiveness() { 51 | logrus.Infof("Liveness probe listening on: %s", port) 52 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 53 | logrus.Debug("ping") 54 | }) 55 | err := http.ListenAndServe(":"+port, nil) // #nosec 56 | if err != nil { 57 | logrus.Errorf("failed to start health probe: %v\n", err) 58 | } 59 | } 60 | 61 | // Run starts the SSO operator 62 | func (o *OperatorOptions) Run() { 63 | namespace := o.Namespace 64 | if namespace == "" { 65 | namespace = os.Getenv(operatorNamespaceEnv) 66 | } 67 | watchNamespace := o.WatchNamespace 68 | if watchNamespace == "" { 69 | watchNamespace = os.Getenv(watchNamespaceEnv) 70 | } 71 | printVersion(namespace, watchNamespace) 72 | 73 | // validate the command line options 74 | err := o.Validate() 75 | if err != nil { 76 | logrus.Errorf("invalid options: %v", err) 77 | os.Exit(2) 78 | } 79 | 80 | opts := &dex.Options{ 81 | HostAndPort: o.DexGrpcHostAndPort, 82 | ClientCrt: o.DexGrpcClientCrt, 83 | ClientKey: o.DexGrpcClientKey, 84 | ClientCA: o.DexGrpcClientCA, 85 | } 86 | dexClient, err := dex.NewClient(opts) 87 | if err != nil { 88 | logrus.Errorf("failed to crate dex client: %v", err) 89 | os.Exit(2) 90 | } 91 | 92 | logrus.Infof("Connected to Dex gRPC server: %s", o.DexGrpcHostAndPort) 93 | 94 | // Register the CRDs 95 | apiclient, err := kubernetes.GetAPIExtensionsClient() 96 | if err != nil { 97 | logrus.Errorf("failed to register the k8s API extensions client: %v", err) 98 | os.Exit(2) 99 | } 100 | err = kubernetes.RegisterSSOCRD(apiclient) 101 | if err != nil { 102 | logrus.Errorf("failed to register the SSO CRD: %v", err) 103 | os.Exit(2) 104 | } 105 | 106 | // configure the operator 107 | sdk.Watch("jenkins.io/v1", "SSO", watchNamespace, 5) 108 | handler, err := operator.NewHandler(dexClient, namespace, o.ClusterRoleName) 109 | if err != nil { 110 | logrus.Errorf("failed to create the operator handler: %v", err) 111 | os.Exit(2) 112 | } 113 | sdk.Handle(handler) 114 | 115 | // start the health probe 116 | go handleLiveness() 117 | 118 | // start the operator 119 | sdk.Run(context.TODO()) 120 | } 121 | 122 | // Validate validates the provided command options 123 | func (o *OperatorOptions) Validate() error { 124 | if o.DexGrpcHostAndPort == "" { 125 | return errors.New("dex gRPC server host and port is empty") 126 | } 127 | if _, err := os.Stat(o.DexGrpcClientCrt); os.IsNotExist(err) { 128 | return fmt.Errorf("provided dex gRPC client cert file '%s' does not exist", o.DexGrpcClientCrt) 129 | } 130 | 131 | if _, err := os.Stat(o.DexGrpcClientKey); os.IsNotExist(err) { 132 | return fmt.Errorf("provided dex gRPC client key file '%s' does not exists", o.DexGrpcClientKey) 133 | } 134 | 135 | if _, err := os.Stat(o.DexGrpcClientCA); os.IsNotExist(err) { 136 | return fmt.Errorf("provided dex gRPC CA cert file '%s' does not exists", o.DexGrpcClientCA) 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func commandRoot() *cobra.Command { 143 | options := &OperatorOptions{} 144 | 145 | rootCmd := &cobra.Command{ 146 | Use: "sso-operator", 147 | Run: func(cmd *cobra.Command, args []string) { 148 | options.Run() 149 | }, 150 | } 151 | 152 | rootCmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "", "Namespace where the operator where the operator is deployed") 153 | rootCmd.Flags().StringVarP(&options.WatchNamespace, "watch-namespace", "", "", "Namespace where the operator will watch for resources (leave empty to watch the entire cluster)") 154 | rootCmd.Flags().StringVarP(&options.DexGrpcHostAndPort, "dex-grpc-host-port", "", "", "Host and port of Dex gRPC server") 155 | rootCmd.Flags().StringVarP(&options.DexGrpcClientCrt, "dex-grpc-client-crt", "", "", "Certificate for Dex gRPC client") 156 | rootCmd.Flags().StringVarP(&options.DexGrpcClientKey, "dex-grpc-client-key", "", "", "Key for Dex gRPC client") 157 | rootCmd.Flags().StringVarP(&options.DexGrpcClientCA, "dex-grpc-client-ca", "", "", "CA certificate for Dex gRPC client") 158 | rootCmd.Flags().StringVarP(&options.ClusterRoleName, "cluster-role-name", "", "", "Cluster role name which has the required permissions for operator") 159 | 160 | return rootCmd 161 | } 162 | 163 | func main() { 164 | if err := commandRoot().Execute(); err != nil { 165 | logrus.Error(err) 166 | os.Exit(2) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sso-operator 2 | 3 | Single Sign-On Kubernetes [operator](https://coreos.com/operators/) for [dex](https://github.com/coreos/dex), which can provision, expose and manage a [SSO proxy](https://github.com/oauth2-proxy/oauth2-proxy) for a Kubernetes service. 4 | 5 | ## Architecture 6 | 7 | ![architecture](images/architecture.png?row=true) 8 | 9 | ## Installation 10 | 11 | ### Using Jenkins X 12 | 13 | You can install the operator and its dependencies with [Jenkins X](https://jenkins-x.io/). The only requirement is to have already allocated a DNS domain for your ingress controller. 14 | 15 | 16 | You can execute the command bellow and then follow the wizard steps: 17 | 18 | ``` 19 | jx create addon sso 20 | ``` 21 | 22 | ### Using Helm 23 | 24 | #### Prerequisites 25 | 26 | The operator requires the [dex](https://github.com/dexidp/dex) identity provider and the [cert-manager](https://github.com/jetstack/cert-manager) version `v.0.6.0` to be installed into your cluster. 27 | You can install `dex`using following [helm chart](https://github.com/jenkins-x/dex/tree/master/charts/dex), which pre-configures the `GitHub connector`, and uses the `cert-manager` service to retrieve 28 | the TLS certificates for dex gRPC API. 29 | 30 | Before starting the installation, you have to create a [GitHub OAuth App](https://github.com/settings/applications/new) which should have as `callback` the *https://DEX_DOMAIN/callback* URL. 31 | 32 | You can install the `dex` chart as follows: 33 | ``` 34 | helm upgrade -i --namespace --wait --timeout 600 dex \ 35 | --set domain="" \ 36 | --set connectors.github.config.clientID="" \ 37 | --set connectors.github.config.clientSecret="" \ 38 | --set connectors.github.config.orgs={ORG1,ORG2} \ 39 | . 40 | ``` 41 | 42 | The web endpoints provided by `dex` IdP have to be publicly exposed and secured with TLS. You can do this pretty easy, if you have the [Jenkins X](https://jenkins-x.io/) installed into your cluster. 43 | 44 | Just executing the command: 45 | 46 | ``` 47 | jx upgrade ingress 48 | ``` 49 | 50 | You can select TLS and provide your `DEX_DOMAIN` and email. This command will configure the ingress controller to fetch automatically the TLS certificate from Let's Encrypt CA server. 51 | 52 | #### Install the operator 53 | 54 | First, you will need to add the jenkins-x chart repository to your helm repositories: 55 | ```sh 56 | helm repo add jenkins-x http://chartmuseum.jenkins-x.io 57 | helm repo update 58 | ``` 59 | 60 | You can now install the chart with: 61 | ``` 62 | helm install --namespace --set dex.grpcHost=dex. --name sso-operator jenkins-x/sso-operator 63 | ``` 64 | 65 | ## Enable Single Sign-On for a service 66 | 67 | After installing the operator, you can enable Single Sign-On for any Kubernetes service by creating a SSO custom resource. 68 | 69 | Let's start by creating a basic Go http service with Jenkins X: 70 | 71 | ``` 72 | jx create quickstart -l Go --name golang-http 73 | ``` 74 | 75 | Within a few minutes, the service should be running in your staging environment. You can view the Kubernetes service created for it with: 76 | 77 | ``` 78 | kubectl get svc -n jx-staging 79 | 80 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 81 | golang-http ClusterIP 10.15.250.117 80/TCP 1m 82 | sso-operator ClusterIP 10.15.244.220 80/TCP 6m 83 | ``` 84 | 85 | You can enable now the Single Sign-On for this service by creating a custom resource as follows: 86 | 87 | ```yaml 88 | cat < or **//** 173 | excludes=**/*sources.jar **/*javadoc.jar 174 | 175 | case.sensitive.glob=false 176 | followSymbolicLinks=true 177 | 178 | ################################## 179 | # Archive properties 180 | ################################## 181 | #archiveExtractionDepth=2 182 | #archiveIncludes=**/*.war **/*.ear 183 | #archiveExcludes=**/*sources.jar 184 | 185 | ################################## 186 | # Proxy settings 187 | ################################## 188 | #proxy.host= 189 | #proxy.port= 190 | #proxy.user= 191 | #proxy.pass= 192 | 193 | ################################## 194 | # SCM settings 195 | ################################## 196 | #scm.type= 197 | #scm.user= 198 | #scm.pass= 199 | #scm.ppk= 200 | #scm.url= 201 | #scm.branch= 202 | #scm.tag= 203 | #scm.npmInstall= 204 | #scm.npmInstallTimeoutMinutes= 205 | #scm.repositoriesFile= 206 | 207 | ############################################## 208 | # SCAN MODE: Linux package manager settings 209 | ############################################## 210 | #scanPackageManager=true 211 | 212 | ################################## 213 | # SCAN MODE: Docker images 214 | ################################## 215 | #docker.scanImages=true 216 | #docker.includes=.*.* 217 | #docker.excludes= 218 | #docker.pull.enable=true 219 | #docker.pull.images=.*.* 220 | #docker.pull.maxImages=10 221 | #docker.pull.tags=.*.* 222 | #docker.pull.digest= 223 | #docker.delete.force=true 224 | #docker.login.sudo=false 225 | 226 | #docker.aws.enable=true 227 | #docker.aws.registryIds= 228 | 229 | #docker.azure.enable=true 230 | #docker.azure.userName= 231 | #docker.azure.userPassword= 232 | #docker.azure.registryNames= 233 | 234 | #docker.artifactory.enable=true 235 | #docker.artifactory.url= 236 | #docker.artifactory.userName= 237 | #docker.artifactory.userPassword= 238 | #docker.artifactory.repositoriesNames= 239 | 240 | ################################## 241 | # SCAN MODE: Docker containers 242 | ################################## 243 | #docker.scanContainers=true 244 | #docker.containerIncludes=.*.* 245 | #docker.containerExcludes= 246 | 247 | ################################ 248 | # Serverless settings 249 | ################################ 250 | #serverless.provider= 251 | #serverless.scanFunctions=true 252 | #serverless.includes= 253 | #serverless.excludes= 254 | #serverless.region= 255 | #serverless.maxFunctions=10 256 | 257 | -------------------------------------------------------------------------------- /pkg/proxy/expose.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "time" 7 | 8 | apiv1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 9 | "github.com/jenkins-x/sso-operator/pkg/kubernetes" 10 | "github.com/operator-framework/operator-sdk/pkg/sdk" 11 | "github.com/pkg/errors" 12 | yaml "gopkg.in/yaml.v2" 13 | batchv1 "k8s.io/api/batch/v1" 14 | "k8s.io/api/core/v1" 15 | apierrors "k8s.io/apimachinery/pkg/api/errors" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | ) 18 | 19 | const ( 20 | exposeImage = "jenkinsxio/exposecontroller" 21 | exposeImageTag = "latest" 22 | exposeCmd = "/exposecontroller" 23 | exposeConfigPath = "/etc/exposecontoller/config.yml" 24 | exposeConfigVolumeName = "expose-config" 25 | exposeConfigMapName = "expose-configmap" 26 | exposeEnv = "KUBERNETES_NAMESPACE" 27 | exposer = "Ingress" 28 | exposeTimeout = time.Duration(5 * time.Minute) 29 | exposeCheckInterval = time.Duration(10 * time.Second) 30 | cleanupeTimeout = time.Duration(2 * time.Minute) 31 | cleanupCheckInterval = time.Duration(10 * time.Second) 32 | ) 33 | 34 | // Expose executes the exposecontroller as a Job in order publicly expose the SSO service 35 | func Expose(sso *apiv1.SSO, serviceName string, serviceAccount string) error { 36 | configMap, err := exposeConfigMap(sso, serviceName) 37 | if err != nil { 38 | return errors.Wrap(err, "building expose config map") 39 | } 40 | configMap.SetOwnerReferences(append(configMap.GetOwnerReferences(), ownerRef(sso))) 41 | err = sdk.Create(configMap) 42 | if err != nil && !apierrors.IsAlreadyExists(err) { 43 | return errors.Wrap(err, "creating expose config map") 44 | } 45 | 46 | job := createJob("expose", sso, serviceAccount, exposeContainer(sso)) 47 | job.SetOwnerReferences(append(job.GetOwnerReferences(), ownerRef(sso))) 48 | err = sdk.Create(job) 49 | if err != nil { 50 | msg := "creating expose job" 51 | if apierrors.IsAlreadyExists(err) { 52 | errdel := sdk.Delete(job) 53 | if errdel != nil { 54 | return errors.Wrapf(errdel, "%s: deleting existing expose job", msg) 55 | } 56 | } 57 | return errors.Wrap(err, msg) 58 | } 59 | 60 | k8sClient, err := kubernetes.GetClientset() 61 | if err != nil { 62 | return errors.Wrap(err, "getting k8s client") 63 | } 64 | err = kubernetes.WaitForJobComplete(k8sClient, sso.GetNamespace(), job.GetName(), exposeCheckInterval, exposeTimeout) 65 | if err != nil { 66 | return errors.Wrap(err, "waiting for SSO to be exposed") 67 | } 68 | 69 | deletePropagation := metav1.DeletePropagationBackground 70 | deleteOption := &metav1.DeleteOptions{ 71 | PropagationPolicy: &deletePropagation, 72 | } 73 | err = sdk.Delete(job, sdk.WithDeleteOptions(deleteOption)) 74 | if err != nil { 75 | return errors.Wrap(err, "cleaning up the expose job") 76 | } 77 | 78 | err = sdk.Delete(configMap) 79 | if err != nil { 80 | return errors.Wrap(err, "cleaning up the expose config map") 81 | } 82 | 83 | return nil 84 | } 85 | 86 | // Cleanup executes the exposecontroller as a job to cleanup the ingress resources 87 | func Cleanup(sso *apiv1.SSO, serviceName string, serviceAccount string) error { 88 | configMap, err := exposeConfigMap(sso, serviceName) 89 | if err != nil && !apierrors.IsAlreadyExists(err) { 90 | return errors.Wrap(err, "building cleanup config map") 91 | } 92 | err = sdk.Create(configMap) 93 | if err != nil && apierrors.IsAlreadyExists(err) { 94 | return errors.Wrap(err, "creating cleanup config map") 95 | } 96 | 97 | job := createJob("cleanup", sso, serviceAccount, cleanupContainer(sso, serviceName)) 98 | err = sdk.Create(job) 99 | if err != nil { 100 | msg := "creating cleanup job" 101 | if apierrors.IsAlreadyExists(err) { 102 | errdel := sdk.Delete(job) 103 | if errdel != nil { 104 | return errors.Wrapf(errdel, "%s: deleting existing cleanup job", msg) 105 | } 106 | } 107 | return errors.Wrap(err, msg) 108 | } 109 | 110 | k8sClient, err := kubernetes.GetClientset() 111 | if err != nil { 112 | return errors.Wrap(err, "getting k8s client") 113 | } 114 | err = kubernetes.WaitForJobComplete(k8sClient, sso.GetNamespace(), job.GetName(), cleanupCheckInterval, cleanupeTimeout) 115 | if err != nil { 116 | return errors.Wrap(err, "waiting for SSO to be cleaned up") 117 | } 118 | 119 | deletePropagation := metav1.DeletePropagationBackground 120 | deleteOption := &metav1.DeleteOptions{ 121 | PropagationPolicy: &deletePropagation, 122 | } 123 | err = sdk.Delete(job, sdk.WithDeleteOptions(deleteOption)) 124 | if err != nil { 125 | return errors.Wrap(err, "deleting the cleanup job") 126 | } 127 | 128 | err = sdk.Delete(configMap) 129 | if err != nil { 130 | return errors.Wrap(err, "deleting the cleanup config map") 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func createJob(name string, sso *apiv1.SSO, serviceAccount string, container *v1.Container) *batchv1.Job { 137 | ns := sso.GetNamespace() 138 | jobName := buildName(sso.GetName(), name) 139 | 140 | podTempl := v1.PodTemplateSpec{ 141 | ObjectMeta: metav1.ObjectMeta{ 142 | Name: jobName, 143 | Namespace: ns, 144 | }, 145 | Spec: v1.PodSpec{ 146 | ServiceAccountName: serviceAccount, 147 | Containers: []v1.Container{*container}, 148 | Volumes: []v1.Volume{{ 149 | Name: exposeConfigVolumeName, 150 | VolumeSource: v1.VolumeSource{ 151 | ConfigMap: &v1.ConfigMapVolumeSource{ 152 | LocalObjectReference: v1.LocalObjectReference{ 153 | Name: exposeConfigMapName, 154 | }, 155 | }, 156 | }, 157 | }}, 158 | RestartPolicy: v1.RestartPolicyNever, 159 | }, 160 | } 161 | 162 | return &batchv1.Job{ 163 | TypeMeta: metav1.TypeMeta{ 164 | Kind: "Job", 165 | APIVersion: "batch/v1", 166 | }, 167 | ObjectMeta: metav1.ObjectMeta{ 168 | Name: jobName, 169 | Namespace: ns, 170 | }, 171 | Spec: batchv1.JobSpec{ 172 | Template: podTempl, 173 | }, 174 | } 175 | } 176 | 177 | func exposeContainer(sso *apiv1.SSO) *v1.Container { 178 | return &v1.Container{ 179 | Name: buildName(sso.GetName(), "expose"), 180 | Image: fmt.Sprintf("%s:%s", exposeImage, exposeImageTag), 181 | ImagePullPolicy: v1.PullIfNotPresent, 182 | Command: []string{exposeCmd}, 183 | Args: []string{fmt.Sprintf("--config=%s", exposeConfigPath), "--v", "4"}, 184 | VolumeMounts: []v1.VolumeMount{{ 185 | Name: exposeConfigVolumeName, 186 | ReadOnly: true, 187 | MountPath: filepath.Dir(exposeConfigPath), 188 | }}, 189 | Env: []v1.EnvVar{{ 190 | Name: exposeEnv, 191 | Value: sso.GetNamespace(), 192 | }}, 193 | } 194 | } 195 | 196 | func cleanupContainer(sso *apiv1.SSO, filter string) *v1.Container { 197 | return &v1.Container{ 198 | Name: buildName(sso.GetName(), "cleanup"), 199 | Image: fmt.Sprintf("%s:%s", exposeImage, exposeImageTag), 200 | ImagePullPolicy: v1.PullIfNotPresent, 201 | Command: []string{exposeCmd}, 202 | Args: []string{fmt.Sprintf("--config=%s", exposeConfigPath), "--cleanup", fmt.Sprintf("--filter=%s", filter)}, 203 | VolumeMounts: []v1.VolumeMount{{ 204 | Name: exposeConfigVolumeName, 205 | ReadOnly: true, 206 | MountPath: filepath.Dir(exposeConfigPath), 207 | }}, 208 | Env: []v1.EnvVar{{ 209 | Name: exposeEnv, 210 | Value: sso.GetNamespace(), 211 | }}, 212 | } 213 | } 214 | 215 | func exposeConfigMap(sso *apiv1.SSO, serviceName string) (*v1.ConfigMap, error) { 216 | exposeConfig := &ExposeConfig{ 217 | Domain: sso.Spec.Domain, 218 | Exposer: exposer, 219 | PathMode: "", 220 | HTTP: false, 221 | TLSAcme: true, 222 | Services: []string{serviceName}, 223 | URLTemplate: sso.Spec.URLTemplate, 224 | } 225 | 226 | config, err := renderExposeConfig(exposeConfig) 227 | if err != nil { 228 | return nil, errors.Wrap(err, "rendering expose config") 229 | } 230 | 231 | return &v1.ConfigMap{ 232 | TypeMeta: metav1.TypeMeta{ 233 | APIVersion: "v1", 234 | Kind: "ConfigMap", 235 | }, 236 | ObjectMeta: metav1.ObjectMeta{ 237 | Name: exposeConfigMapName, 238 | Namespace: sso.GetNamespace(), 239 | }, 240 | Data: map[string]string{ 241 | filepath.Base(exposeConfigPath): config, 242 | }, 243 | }, nil 244 | } 245 | 246 | // ExposeConfig holds the configuration for exposecontroller 247 | type ExposeConfig struct { 248 | Domain string `yaml:"domain,omitempty" json:"domain"` 249 | Exposer string `yaml:"exposer" json:"exposer"` 250 | PathMode string `yaml:"path-mode" json:"path_mode"` 251 | HTTP bool `yaml:"http" json:"http"` 252 | TLSAcme bool `yaml:"tls-acme" json:"tls_acme"` 253 | Services []string `yaml:"services,omitempty" json:"services"` 254 | URLTemplate string `yaml:"urltemplate,omitempty" json:"urltemplate"` 255 | } 256 | 257 | func renderExposeConfig(config *ExposeConfig) (string, error) { 258 | b, err := yaml.Marshal(config) 259 | if err != nil { 260 | return "", errors.Wrap(err, "marshaling expose config to YAML") 261 | } 262 | return string(b), nil 263 | } 264 | -------------------------------------------------------------------------------- /pkg/kubernetes/wait.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 The Skaffold 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 | package kubernetes 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | "github.com/sirupsen/logrus" 24 | appsv1 "k8s.io/api/apps/v1" 25 | batchv1 "k8s.io/api/batch/v1" 26 | v1 "k8s.io/api/core/v1" 27 | apierrs "k8s.io/apimachinery/pkg/api/errors" 28 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/fields" 30 | "k8s.io/apimachinery/pkg/labels" 31 | "k8s.io/apimachinery/pkg/runtime/schema" 32 | "k8s.io/apimachinery/pkg/util/wait" 33 | "k8s.io/apimachinery/pkg/watch" 34 | "k8s.io/client-go/kubernetes" 35 | corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 36 | ) 37 | 38 | // WaitForPodReady waits for given POD to become ready 39 | func WaitForPodReady(pods corev1.PodInterface, podName string) error { 40 | logrus.Infof("Waiting for %s to be scheduled", podName) 41 | err := wait.PollImmediate(time.Second, time.Minute*2, func() (bool, error) { 42 | _, err := pods.Get(podName, meta_v1.GetOptions{ 43 | IncludeUninitialized: true, 44 | }) 45 | if err != nil { 46 | logrus.Infof("Getting pod: %v.", err) 47 | return false, nil 48 | } 49 | return true, nil 50 | }) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | logrus.Infof("Waiting for %s to be ready.", podName) 56 | return wait.PollImmediate(time.Second, time.Minute*10, func() (bool, error) { 57 | pod, err := pods.Get(podName, meta_v1.GetOptions{ 58 | IncludeUninitialized: true, 59 | }) 60 | if err != nil { 61 | return false, fmt.Errorf("not found: %s", podName) 62 | } 63 | switch pod.Status.Phase { 64 | case v1.PodRunning: 65 | for _, cs := range pod.Status.ContainerStatuses { 66 | if !cs.Ready { 67 | logrus.Infof("Container %s is still in state %s.", cs.Name, cs.State.String()) 68 | return false, nil 69 | } 70 | } 71 | return true, nil 72 | case v1.PodSucceeded, v1.PodFailed: 73 | return false, fmt.Errorf("pod already in terminal phase: %s", pod.Status.Phase) 74 | case v1.PodUnknown, v1.PodPending: 75 | return false, nil 76 | } 77 | return false, fmt.Errorf("unknown phase: %s", pod.Status.Phase) 78 | }) 79 | } 80 | 81 | // WaitForPodComplete wait for given POD to complete 82 | func WaitForPodComplete(pods corev1.PodInterface, podName string, timeout time.Duration) error { 83 | logrus.Infof("Waiting for %s to be ready", podName) 84 | return wait.PollImmediate(time.Millisecond*500, timeout, func() (bool, error) { 85 | pod, err := pods.Get(podName, meta_v1.GetOptions{ 86 | IncludeUninitialized: true, 87 | }) 88 | if err != nil { 89 | logrus.Infof("Getting pod %s.", err) 90 | return false, nil 91 | } 92 | switch pod.Status.Phase { 93 | case v1.PodSucceeded: 94 | return true, nil 95 | case v1.PodRunning: 96 | return false, nil 97 | case v1.PodFailed: 98 | return false, fmt.Errorf("pod already in terminal phase: %s", pod.Status.Phase) 99 | case v1.PodUnknown, v1.PodPending: 100 | return false, nil 101 | } 102 | return false, fmt.Errorf("unknown phase: %s", pod.Status.Phase) 103 | }) 104 | } 105 | 106 | // WaitForPodsWithLabelRunning waits for all matching pods to become Running and at least one matching pod exists. 107 | func WaitForPodsWithLabelRunning(c kubernetes.Interface, namespace string, label labels.Selector, timeout time.Duration) error { 108 | lastKnownPodNumber := -1 109 | return wait.Poll(time.Second*3, timeout, func() (bool, error) { 110 | listOpts := meta_v1.ListOptions{LabelSelector: label.String()} 111 | pods, err := c.CoreV1().Pods(namespace).List(listOpts) 112 | if err != nil { 113 | logrus.Infof("Error getting Pods with label selector %q [%v]\n", label.String(), err) 114 | return false, nil 115 | } 116 | 117 | if len(pods.Items) == 0 { 118 | return false, nil 119 | } 120 | 121 | if lastKnownPodNumber != len(pods.Items) { 122 | logrus.Infof("Found %d Pods for label selector %s.\n", len(pods.Items), label.String()) 123 | lastKnownPodNumber = len(pods.Items) 124 | } 125 | 126 | for _, pod := range pods.Items { 127 | phase := pod.Status.Phase 128 | if phase != v1.PodRunning { 129 | logrus.Infof("Pod %s still in state %s.", pod.GetName(), phase) 130 | return false, nil 131 | } 132 | for _, cs := range pod.Status.ContainerStatuses { 133 | if !cs.Ready { 134 | logrus.Infof("Pod %s still has container %s not running.", pod.GetName(), cs.Name) 135 | return false, nil 136 | } 137 | } 138 | } 139 | 140 | return true, nil 141 | }) 142 | } 143 | 144 | // WaitForDeploymentToStabilize waits till the Deployment has a matching generation/replica count between spec and status. 145 | func WaitForDeploymentToStabilize(c kubernetes.Interface, namespace, name string, timeout time.Duration) error { 146 | options := meta_v1.ListOptions{FieldSelector: fields.Set{ 147 | "metadata.name": name, 148 | "metadata.namespace": namespace, 149 | }.AsSelector().String()} 150 | w, err := c.AppsV1().Deployments(namespace).Watch(options) 151 | if err != nil { 152 | return err 153 | } 154 | _, err = watch.Until(timeout, w, func(event watch.Event) (bool, error) { 155 | switch event.Type { 156 | case watch.Deleted: 157 | return false, apierrs.NewNotFound(schema.GroupResource{Resource: "deployments"}, "") 158 | } 159 | switch dp := event.Object.(type) { 160 | case *appsv1.Deployment: 161 | if dp.Name == name && dp.Namespace == namespace && 162 | dp.Generation <= dp.Status.ObservedGeneration && 163 | *(dp.Spec.Replicas) == dp.Status.Replicas { 164 | logrus.Infof("Deployment %s in namespace %s ready.", name, namespace) 165 | return true, nil 166 | } 167 | logrus.Infof("Waiting for deployment %s to stabilize, generation %v observed generation %v spec.replicas %d status.replicas %d.", 168 | name, dp.Generation, dp.Status.ObservedGeneration, *(dp.Spec.Replicas), dp.Status.Replicas) 169 | } 170 | return false, nil 171 | }) 172 | return err 173 | } 174 | 175 | // WaitForService waits until the service appears (exist == true), or disappears (exist == false) 176 | func WaitForService(c kubernetes.Interface, namespace, name string, exist bool, interval, timeout time.Duration) error { 177 | err := wait.PollImmediate(interval, timeout, func() (bool, error) { 178 | _, err := c.CoreV1().Services(namespace).Get(name, meta_v1.GetOptions{}) 179 | switch { 180 | case err == nil: 181 | logrus.Infof("Service %s in namespace %s found.", name, namespace) 182 | return exist, nil 183 | case apierrs.IsNotFound(err): 184 | logrus.Infof("Service %s in namespace %s disappeared.", name, namespace) 185 | return !exist, nil 186 | case !IsRetryableAPIError(err): 187 | logrus.Infof("Non-retryable failure while getting service.") 188 | return false, err 189 | default: 190 | logrus.Infof("Get service %s in namespace %s failed: %v.", name, namespace, err) 191 | return false, nil 192 | } 193 | }) 194 | if err != nil { 195 | stateMsg := map[bool]string{true: "to appear", false: "to disappear"} 196 | return fmt.Errorf("error waiting for service %s/%s %s: %v", namespace, name, stateMsg[exist], err) 197 | } 198 | return nil 199 | } 200 | 201 | // WaitForServiceEndpointsNum waits until the amount of endpoints that implement service to expectNum. 202 | func WaitForServiceEndpointsNum(c kubernetes.Interface, namespace, serviceName string, expectNum int, interval, timeout time.Duration) error { 203 | return wait.Poll(interval, timeout, func() (bool, error) { 204 | logrus.Infof("Waiting for amount of service:%s endpoints to be %d.", serviceName, expectNum) 205 | list, err := c.CoreV1().Endpoints(namespace).List(meta_v1.ListOptions{}) 206 | if err != nil { 207 | return false, err 208 | } 209 | 210 | for _, e := range list.Items { 211 | if e.Name == serviceName && countEndpointsNum(&e) == expectNum { // #nosec 212 | return true, nil // #nosec 213 | } // #nosec 214 | } 215 | return false, nil 216 | }) 217 | } 218 | 219 | func countEndpointsNum(e *v1.Endpoints) int { 220 | num := 0 221 | for _, sub := range e.Subsets { 222 | num += len(sub.Addresses) 223 | } 224 | return num 225 | } 226 | 227 | // IsRetryableAPIError indicates if the given error is retryable 228 | func IsRetryableAPIError(err error) bool { 229 | return apierrs.IsTimeout(err) || apierrs.IsServerTimeout(err) || apierrs.IsTooManyRequests(err) || apierrs.IsInternalError(err) 230 | } 231 | 232 | // WaitForJobComplete wait for a job to complete 233 | func WaitForJobComplete(c kubernetes.Interface, namespace, name string, interval, timeout time.Duration) error { 234 | err := wait.PollImmediate(interval, timeout, func() (bool, error) { 235 | job, err := c.BatchV1().Jobs(namespace).Get(name, meta_v1.GetOptions{}) 236 | switch { 237 | case err == nil: 238 | conditions := job.Status.Conditions 239 | if len(conditions) == 0 { 240 | return false, nil 241 | } 242 | for _, condition := range conditions { 243 | if condition.Type != batchv1.JobComplete { 244 | return false, fmt.Errorf("job failed: %s", condition.Message) 245 | } 246 | } 247 | logrus.Infof("Job %s in namespace %s is completed.", name, namespace) 248 | return true, nil 249 | case apierrs.IsNotFound(err): 250 | logrus.Infof("Job %s in namespace %s not found.", name, namespace) 251 | return false, nil 252 | case !IsRetryableAPIError(err): 253 | logrus.Infof("Non-retryable failure while getting job.") 254 | return false, err 255 | default: 256 | logrus.Infof("Get job %s in namespace %s failed: %v.", name, namespace, err) 257 | return false, nil 258 | } 259 | }) 260 | if err != nil { 261 | return fmt.Errorf("error waiting for job %s/%s: %v", namespace, name, err) 262 | } 263 | return nil 264 | } 265 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2018] [Jenkins X Team] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pkg/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "fmt" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/dexidp/dex/api" 13 | apiv1 "github.com/jenkins-x/sso-operator/pkg/apis/jenkins.io/v1" 14 | "github.com/jenkins-x/sso-operator/pkg/kubernetes" 15 | "github.com/operator-framework/operator-sdk/pkg/sdk" 16 | "github.com/pkg/errors" 17 | appsv1 "k8s.io/api/apps/v1" 18 | v1 "k8s.io/api/core/v1" 19 | apierrors "k8s.io/apimachinery/pkg/api/errors" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | k8slabels "k8s.io/apimachinery/pkg/labels" 22 | "k8s.io/apimachinery/pkg/util/intstr" 23 | ) 24 | 25 | const ( 26 | configPath = "/config/oauth2_proxy.cfg" 27 | configVolumeName = "proxy-config" 28 | configSecretName = "proxy-secret" // #nosec 29 | secretVersionEnv = "SECRET_VERSION" 30 | portName = "proxy-port" 31 | port = 4180 32 | healthPath = "/ping" 33 | replicas = 1 34 | publicPort = 80 35 | cookieSecretLen = 32 36 | fakeURL = "https://fake-oauth2-proxy" 37 | createTimeout = time.Duration(60 * time.Second) 38 | createIntervalCheck = time.Duration(10 * time.Second) 39 | readyTimeout = time.Duration(5 * time.Minute) 40 | appLabel = "app" 41 | releaseLabel = "release" 42 | 43 | exposeAnnotation = "fabric8.io/expose" 44 | exposeIngressAnnotation = "fabric8.io/ingress.annotations" 45 | ingressNameAnnotation = "fabric8.io/ingress.name" 46 | ingressClassAnnotations = "kubernetes.io/ingress.class" 47 | oldCertManagerAnnotation = "certmanager.k8s.io/issuer" 48 | certManagerAnnotation = "cert-manager.io/issuer" 49 | ingressClass = "nginx" 50 | ) 51 | 52 | // Proxy keeps the k8s resources created for a proxy 53 | type Proxy struct { 54 | AppName string 55 | Secret *v1.Secret 56 | Deployment *appsv1.Deployment 57 | Service *v1.Service 58 | } 59 | 60 | // FakeRedirectURL builds a fake redirect URL for oauth2 proxy 61 | func FakeRedirectURL() string { 62 | return RedirectURL(fakeURL) 63 | } 64 | 65 | // RedirectURL build the redirect URL for oauth2 proxy 66 | func RedirectURL(URL string) string { 67 | return fmt.Sprintf("%s/oauth2/callback", URL) 68 | } 69 | 70 | // ConvertHostsToRedirectURLs converts a list of host to proxy redirect URLs 71 | func ConvertHostsToRedirectURLs(hosts []string, sso *apiv1.SSO) []string { 72 | redirectURLs := []string{} 73 | for _, host := range hosts { 74 | redirectURL := RedirectURL(fmt.Sprintf("https://%s", host)) 75 | redirectURLs = append(redirectURLs, redirectURL) 76 | } 77 | return redirectURLs 78 | } 79 | 80 | // buildName concatenates resourceName and suffix equally with a max length of 63 chars 81 | func buildName(resourceName string, suffix string) string { 82 | name := resourceName 83 | 84 | maxLenName := 62 85 | maxLenOnePart := maxLenName / 2 86 | switch { 87 | case len(suffix) > 0 && len(name)+len(suffix) < maxLenName: 88 | return fmt.Sprintf("%s-%s", name, suffix) 89 | case len(suffix) == 0 && len(name) < maxLenName: 90 | return name 91 | case len(suffix) > maxLenOnePart && len(name) > maxLenOnePart: 92 | name = strings.TrimSuffix(name[:maxLenOnePart], "-") 93 | suffix = strings.TrimSuffix(suffix[:maxLenOnePart], "-") 94 | case len(suffix) > maxLenOnePart && len(name) < maxLenOnePart: 95 | suffix = strings.TrimSuffix(suffix[:62-len(name)], "-") 96 | case len(suffix) < maxLenOnePart && len(name) > maxLenOnePart: 97 | name = strings.TrimSuffix(name[:maxLenName-len(suffix)], "-") 98 | case len(name) > maxLenName+1: 99 | name = strings.TrimSuffix(name[:maxLenName], "-") 100 | } 101 | 102 | if suffix != "" { 103 | return fmt.Sprintf("%s-%s", name, suffix) 104 | } 105 | return name 106 | } 107 | 108 | func labels(sso *apiv1.SSO, appName string) map[string]string { 109 | return map[string]string{"app": appName, "sso": sso.GetName()} 110 | } 111 | 112 | func serviceAnnotations(sso *apiv1.SSO, appName string) map[string]string { 113 | expIngressAnnotation := ingressClassAnnotations + ": " + ingressClass 114 | if len(sso.Spec.CertIssuerName) != 0 { 115 | expIngressAnnotation += "\n" + oldCertManagerAnnotation + ": " + sso.Spec.CertIssuerName 116 | expIngressAnnotation += "\n" + certManagerAnnotation + ": " + sso.Spec.CertIssuerName 117 | } 118 | return map[string]string{ 119 | exposeAnnotation: "true", 120 | ingressNameAnnotation: appName, 121 | exposeIngressAnnotation: expIngressAnnotation, 122 | } 123 | } 124 | 125 | // Deploy deploys the oauth2 proxy 126 | func Deploy(sso *apiv1.SSO, oidcClient *api.Client, cookieSecret string) (*Proxy, error) { 127 | appName, err := getAppName(sso.Spec.UpstreamService, sso.GetNamespace()) 128 | if err != nil { 129 | return nil, errors.Wrap(err, "gettting the app name from upstream service labels") 130 | } 131 | secret, err := proxySecret(sso, oidcClient, cookieSecret, labels(sso, appName)) 132 | if err != nil { 133 | return nil, errors.Wrap(err, "creating oauth2_proxy config") 134 | } 135 | secret.SetOwnerReferences(append(secret.GetOwnerReferences(), ownerRef(sso))) 136 | err = sdk.Create(secret) 137 | if err != nil && !apierrors.IsAlreadyExists(err) { 138 | return nil, errors.Wrap(err, "creating oauth2_proxy secret") 139 | } 140 | 141 | ns := sso.GetNamespace() 142 | secretVersion := computeSecretVersion(secret) 143 | podTempl := v1.PodTemplateSpec{ 144 | ObjectMeta: metav1.ObjectMeta{ 145 | Name: buildName(sso.GetName(), ""), 146 | Namespace: ns, 147 | Labels: labels(sso, appName), 148 | }, 149 | Spec: v1.PodSpec{ 150 | Containers: []v1.Container{proxyContainer(sso, secretVersion)}, 151 | Volumes: []v1.Volume{{ 152 | Name: configVolumeName, 153 | VolumeSource: v1.VolumeSource{ 154 | Secret: &v1.SecretVolumeSource{ 155 | SecretName: buildName(sso.GetName(), configSecretName), 156 | }, 157 | }, 158 | }}, 159 | ImagePullSecrets: []v1.LocalObjectReference{ 160 | {Name: sso.Spec.ProxyImagePullSecret}, 161 | }, 162 | }, 163 | } 164 | 165 | deployment := buildName(sso.GetName(), "") 166 | var replicas int32 = replicas 167 | d := &appsv1.Deployment{ 168 | TypeMeta: metav1.TypeMeta{ 169 | Kind: "Deployment", 170 | APIVersion: "apps/v1", 171 | }, 172 | ObjectMeta: metav1.ObjectMeta{ 173 | Name: deployment, 174 | Namespace: ns, 175 | Labels: labels(sso, appName), 176 | }, 177 | Spec: appsv1.DeploymentSpec{ 178 | Replicas: &replicas, 179 | Selector: &metav1.LabelSelector{MatchLabels: labels(sso, appName)}, 180 | Template: podTempl, 181 | Strategy: appsv1.DeploymentStrategy{ 182 | Type: appsv1.RollingUpdateDeploymentStrategyType, 183 | RollingUpdate: &appsv1.RollingUpdateDeployment{ 184 | MaxUnavailable: func(a intstr.IntOrString) *intstr.IntOrString { return &a }(intstr.FromInt(1)), 185 | MaxSurge: func(a intstr.IntOrString) *intstr.IntOrString { return &a }(intstr.FromInt(1)), 186 | }, 187 | }, 188 | }, 189 | } 190 | 191 | d.SetOwnerReferences(append(d.GetOwnerReferences(), ownerRef(sso))) 192 | 193 | err = sdk.Create(d) 194 | if err != nil && !apierrors.IsAlreadyExists(err) { 195 | return nil, errors.Wrap(err, "creating oauth2_proxy deployment") 196 | } 197 | 198 | service := sso.GetName() 199 | svc := &v1.Service{ 200 | TypeMeta: metav1.TypeMeta{ 201 | Kind: "Service", 202 | APIVersion: "v1", 203 | }, 204 | ObjectMeta: metav1.ObjectMeta{ 205 | Name: service, 206 | Namespace: ns, 207 | Labels: labels(sso, appName), 208 | Annotations: serviceAnnotations(sso, appName), 209 | }, 210 | Spec: v1.ServiceSpec{ 211 | Ports: []v1.ServicePort{{ 212 | Name: portName, 213 | Protocol: v1.ProtocolTCP, 214 | Port: publicPort, 215 | TargetPort: intstr.FromInt(port), 216 | }}, 217 | Selector: labels(sso, appName), 218 | }, 219 | } 220 | 221 | svc.SetOwnerReferences(append(svc.GetOwnerReferences(), ownerRef(sso))) 222 | 223 | err = sdk.Create(svc) 224 | if err != nil && !apierrors.IsAlreadyExists(err) { 225 | return nil, errors.Wrap(err, "creating oauth2_proxy service") 226 | } 227 | 228 | k8sClient, err := kubernetes.GetClientset() 229 | if err != nil { 230 | return nil, errors.Wrap(err, "getting k8s client") 231 | } 232 | 233 | exists := true 234 | err = kubernetes.WaitForService(k8sClient, ns, service, exists, createIntervalCheck, createTimeout) 235 | if err != nil { 236 | return nil, errors.Wrap(err, "wait for service") 237 | } 238 | 239 | label := k8slabels.SelectorFromSet(k8slabels.Set(map[string]string{"sso": sso.GetName()})) 240 | err = kubernetes.WaitForPodsWithLabelRunning(k8sClient, ns, label, readyTimeout) 241 | if err != nil { 242 | return nil, errors.Wrap(err, "waiting for SSO proxy") 243 | } 244 | 245 | return &Proxy{ 246 | AppName: appName, 247 | Secret: secret, 248 | Deployment: d, 249 | Service: svc, 250 | }, nil 251 | } 252 | 253 | // Update updates the oauth2_proxy secret and deployment 254 | func Update(proxy *Proxy, sso *apiv1.SSO, client *api.Client, cookieSecret string) error { 255 | err := updateProxySecret(proxy.Secret, sso, client, cookieSecret) 256 | if err != nil { 257 | return errors.Wrap(err, "updating oauth2_proxy secret") 258 | } 259 | 260 | k8sClient, err := kubernetes.GetClientset() 261 | if err != nil { 262 | return errors.Wrap(err, "getting k8s client") 263 | } 264 | 265 | namespace := sso.GetNamespace() 266 | deploymentList, err := k8sClient.AppsV1().Deployments(namespace).List(metav1.ListOptions{}) 267 | if err != nil { 268 | return errors.Wrap(err, "listing deployments") 269 | } 270 | secretVersion := computeSecretVersion(proxy.Secret) 271 | deploymentName := proxy.Deployment.GetName() 272 | for _, deployment := range deploymentList.Items { 273 | if deployment.GetName() == deploymentName { 274 | containers := deployment.Spec.Template.Spec.Containers 275 | for _, container := range containers { 276 | updateContainer(&container, secretVersion) // #nosec 277 | } 278 | deployment.TypeMeta = metav1.TypeMeta{ 279 | Kind: "Deployment", 280 | APIVersion: "apps/v1", 281 | } 282 | err = sdk.Update(&deployment) // #nosec 283 | if err != nil { 284 | return errors.Wrap(err, "updating oauth2_proxy deployment") 285 | } 286 | } 287 | } 288 | 289 | label := k8slabels.SelectorFromSet(k8slabels.Set(map[string]string{"sso": sso.GetName()})) 290 | err = kubernetes.WaitForPodsWithLabelRunning(k8sClient, sso.GetNamespace(), label, readyTimeout) 291 | if err != nil { 292 | return errors.Wrap(err, "waiting for SSO proxy") 293 | } 294 | return nil 295 | } 296 | 297 | func updateContainer(container *v1.Container, secretVersion string) { 298 | for i, env := range container.Env { 299 | if env.Name == secretVersionEnv { 300 | container.Env[i].Value = secretVersion 301 | } 302 | } 303 | } 304 | 305 | func computeSecretVersion(secret *v1.Secret) string { 306 | secretData := "" 307 | for k, v := range secret.StringData { 308 | secretData += k + v 309 | } 310 | hash := sha256.Sum256([]byte(secretData)) 311 | secretVersion := base64.URLEncoding.EncodeToString(hash[:]) 312 | return secretVersion 313 | } 314 | 315 | func ownerRef(sso *apiv1.SSO) metav1.OwnerReference { 316 | controller := true 317 | return metav1.OwnerReference{ 318 | APIVersion: apiv1.SchemeGroupVersion.String(), 319 | Kind: apiv1.SSOKind, 320 | Name: sso.Name, 321 | UID: sso.UID, 322 | Controller: &controller, 323 | } 324 | } 325 | 326 | func proxyContainer(sso *apiv1.SSO, secretVersion string) v1.Container { 327 | args := []string{fmt.Sprintf("--config=%s", configPath)} 328 | // should only be used in testing scenarios 329 | if sso.Spec.SSLInsecureSkipVerify { 330 | args = append(args, "--ssl-insecure-skip-verify=true") 331 | } 332 | return v1.Container{ 333 | Name: buildName(sso.GetName(), ""), 334 | Image: fmt.Sprintf("%s:%s", sso.Spec.ProxyImage, sso.Spec.ProxyImageTag), 335 | ImagePullPolicy: v1.PullIfNotPresent, 336 | Args: args, 337 | Ports: []v1.ContainerPort{{ 338 | Name: portName, 339 | ContainerPort: int32(port), 340 | Protocol: v1.ProtocolTCP, 341 | }}, 342 | Resources: sso.Spec.ProxyResources, 343 | VolumeMounts: []v1.VolumeMount{{ 344 | Name: configVolumeName, 345 | ReadOnly: true, 346 | MountPath: filepath.Dir(configPath), 347 | }}, 348 | Env: []v1.EnvVar{{ 349 | Name: secretVersionEnv, 350 | Value: secretVersion, 351 | }}, 352 | LivenessProbe: &v1.Probe{ 353 | Handler: v1.Handler{ 354 | HTTPGet: &v1.HTTPGetAction{ 355 | Path: healthPath, 356 | Port: intstr.FromInt(port), 357 | Scheme: v1.URISchemeHTTP, 358 | }, 359 | }, 360 | InitialDelaySeconds: 60, 361 | TimeoutSeconds: 10, 362 | PeriodSeconds: 60, 363 | FailureThreshold: 3, 364 | }, 365 | ReadinessProbe: &v1.Probe{ 366 | Handler: v1.Handler{ 367 | HTTPGet: &v1.HTTPGetAction{ 368 | Path: healthPath, 369 | Port: intstr.FromInt(port), 370 | Scheme: v1.URISchemeHTTP, 371 | }, 372 | }, 373 | InitialDelaySeconds: 30, 374 | TimeoutSeconds: 10, 375 | PeriodSeconds: 10, 376 | FailureThreshold: 3, 377 | }, 378 | } 379 | } 380 | 381 | func proxyConfig(sso *apiv1.SSO, client *api.Client, cookieSecret string) (string, error) { 382 | upstreamURL, err := getUpstreamURL(sso.Spec.UpstreamService, sso.Namespace) 383 | if err != nil { 384 | return "", errors.Wrap(err, "getting the upstream service URL") 385 | } 386 | redirectURLs := client.RedirectUris 387 | if len(redirectURLs) == 0 { 388 | return "", errors.New("no redirect URL provided") 389 | } 390 | issuerURL := sso.Spec.OIDCIssuerURL 391 | if !strings.HasPrefix(issuerURL, "https://") { 392 | return "", errors.New("issuer URL must used HTTPS") 393 | } 394 | c := &Config{ 395 | Port: port, 396 | ClientID: client.GetId(), 397 | ClientSecret: client.GetSecret(), 398 | OIDCIssuerURL: sso.Spec.OIDCIssuerURL, 399 | RedirectURL: redirectURLs[0], 400 | LoginURL: fmt.Sprintf("%s/auth", issuerURL), 401 | RedeemURL: fmt.Sprintf("%s/token", issuerURL), 402 | Upstream: upstreamURL, 403 | ForwardToken: sso.Spec.ForwardToken, 404 | Cookie: Cookie{ 405 | Name: sso.Spec.CookieSpec.Name, 406 | Secret: cookieSecret, 407 | Domain: sso.Spec.Domain, 408 | Expire: sso.Spec.CookieSpec.Expire, 409 | Refresh: sso.Spec.CookieSpec.Refresh, 410 | Secure: sso.Spec.CookieSpec.Secure, 411 | HTTPOnly: sso.Spec.CookieSpec.HTTPOnly, 412 | }, 413 | } 414 | 415 | config, err := renderConfig(c) 416 | if err != nil { 417 | return "", errors.Wrap(err, "rendering oauth2_proxy config") 418 | } 419 | return config, nil 420 | } 421 | 422 | func updateProxySecret(secret *v1.Secret, sso *apiv1.SSO, client *api.Client, cookieSecret string) error { 423 | config, err := proxyConfig(sso, client, cookieSecret) 424 | if err != nil { 425 | return errors.Wrap(err, "creating oauth2_proxy config") 426 | } 427 | 428 | secret.StringData[filepath.Base(configPath)] = config 429 | 430 | err = sdk.Update(secret) 431 | if err != nil { 432 | return errors.Wrap(err, "updating oauth2_proxy secret") 433 | } 434 | return nil 435 | } 436 | 437 | func proxySecret(sso *apiv1.SSO, client *api.Client, cookieSecret string, labels map[string]string) (*v1.Secret, error) { 438 | config, err := proxyConfig(sso, client, cookieSecret) 439 | if err != nil { 440 | return nil, errors.Wrap(err, "creating oauth2_proxy config") 441 | } 442 | secret := &v1.Secret{ 443 | TypeMeta: metav1.TypeMeta{ 444 | APIVersion: "v1", 445 | Kind: "Secret", 446 | }, 447 | ObjectMeta: metav1.ObjectMeta{ 448 | Name: buildName(sso.GetName(), configSecretName), 449 | Namespace: sso.Namespace, 450 | Labels: labels, 451 | }, 452 | StringData: map[string]string{ 453 | filepath.Base(configPath): config, 454 | }, 455 | Type: v1.SecretTypeOpaque, 456 | } 457 | return secret, nil 458 | } 459 | 460 | // GenerateCookieKey generates a random key which used to sign the SSO cookie 461 | func GenerateCookieKey() (string, error) { 462 | return generateSecret(cookieSecretLen) 463 | } 464 | 465 | func generateSecret(size int) (string, error) { 466 | const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 467 | bytes, err := generateRandomBytes(size) 468 | if err != nil { 469 | return "", errors.Wrap(err, "generating secret") 470 | } 471 | for i, b := range bytes { 472 | bytes[i] = letters[b%byte(len(letters))] 473 | } 474 | return string(bytes), nil 475 | } 476 | 477 | func generateRandomBytes(len int) ([]byte, error) { 478 | b := make([]byte, len) 479 | _, err := rand.Read(b) 480 | if err != nil { 481 | return nil, errors.Wrap(err, "generating random") 482 | } 483 | return b, nil 484 | } 485 | 486 | func getUpstreamURL(upstreamService string, namespace string) (string, error) { 487 | kubeClient, err := kubernetes.GetClientset() 488 | if err != nil { 489 | return "", errors.Wrap(err, "creating k8s client") 490 | } 491 | 492 | serviceList, err := kubeClient.CoreV1().Services(namespace).List(metav1.ListOptions{}) 493 | if err != nil { 494 | return "", errors.Wrapf(err, "listing services in namespace '%s'", namespace) 495 | } 496 | for _, service := range serviceList.Items { 497 | if service.GetName() == upstreamService { 498 | port := service.Spec.Ports[0].Port 499 | return fmt.Sprintf("http://%s:%d", service.Name, port), nil 500 | } 501 | } 502 | return "", fmt.Errorf("no service '%s' found in namespace '%s'", upstreamService, namespace) 503 | } 504 | 505 | func getAppName(upstreamService string, namespace string) (string, error) { 506 | kubeClient, err := kubernetes.GetClientset() 507 | if err != nil { 508 | return "", errors.Wrap(err, "creating k8s client") 509 | } 510 | serviceList, err := kubeClient.CoreV1().Services(namespace).List(metav1.ListOptions{}) 511 | if err != nil { 512 | return "", errors.Wrapf(err, "listing services in namespace '%s'", namespace) 513 | } 514 | for _, service := range serviceList.Items { 515 | if service.GetName() == upstreamService { 516 | labels := service.ObjectMeta.GetLabels() 517 | appLabelValue := "" 518 | releaseLabelValue := "" 519 | for name, value := range labels { 520 | if name == appLabel { 521 | appLabelValue = value 522 | break 523 | } 524 | if name == releaseLabel { 525 | releaseLabelValue = value 526 | } 527 | } 528 | if appLabelValue != "" { 529 | return appLabelValue, nil 530 | } 531 | if releaseLabelValue != "" { 532 | return strings.Replace(service.Name, releaseLabelValue+"-", "", 1), nil 533 | } 534 | return service.Name, nil 535 | } 536 | } 537 | return "", fmt.Errorf("no service '%s' found in namespace '%s'", upstreamService, namespace) 538 | } 539 | --------------------------------------------------------------------------------