From 1faba26281049619c2ec6c2e8a3eadabcfc98b3c Mon Sep 17 00:00:00 2001 From: Kumbirai Tanekha Date: Tue, 24 May 2022 17:04:32 +0000 Subject: [PATCH 01/14] Rudimentary support for authn-k8s namespace label selector --- Gemfile | 2 + Gemfile.lock | 5 + .../authn_k8s/authentication_request.rb | 4 +- app/domain/authentication/authn_k8s/consts.rb | 7 +- .../authn_k8s/k8s_object_lookup.rb | 6 + .../authn_k8s/k8s_resource_validator.rb | 7 + .../required_exclusive_constraint.rb | 17 + app/domain/errors.rb | 10 + spec/controllers/authn_k8s_test_server.rb | 118 +++ .../controllers/bad:api.v1.getnamespaces.json | 8 + spec/controllers/good:api.json | 12 + .../good:api.v1.getnamespaces.json | 89 ++ spec/controllers/good:api.v1.getpod.json | 420 +++++++++ spec/controllers/good:api.v1.json | 548 +++++++++++ spec/controllers/good:apis.all.json | 41 + spec/controllers/good:apis.json | 868 ++++++++++++++++++ spec/controllers/unauthorized.json | 9 + 17 files changed, 2167 insertions(+), 4 deletions(-) create mode 100644 app/domain/authentication/constraints/required_exclusive_constraint.rb create mode 100644 spec/controllers/authn_k8s_test_server.rb create mode 100644 spec/controllers/bad:api.v1.getnamespaces.json create mode 100644 spec/controllers/good:api.json create mode 100644 spec/controllers/good:api.v1.getnamespaces.json create mode 100644 spec/controllers/good:api.v1.getpod.json create mode 100644 spec/controllers/good:api.v1.json create mode 100644 spec/controllers/good:apis.all.json create mode 100644 spec/controllers/good:apis.json create mode 100644 spec/controllers/unauthorized.json diff --git a/Gemfile b/Gemfile index 607ab39985..2e9dc14cb7 100644 --- a/Gemfile +++ b/Gemfile @@ -113,6 +113,8 @@ group :development, :test do gem 'vcr' gem 'webmock' gem 'webrick' + gem 'faye-websocket' + gem 'thin' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 9e670b1cea..ee548f5816 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,6 +168,7 @@ GEM cucumber-core (~> 10.1, >= 10.1.0) cucumber-cucumber-expressions (~> 14.0, >= 14.0.0) cucumber-messages (~> 17.1, >= 17.1.1) + daemons (1.4.1) database_cleaner (1.8.5) debase (0.2.5.beta2) debase-ruby_core_source (>= 0.10.12) @@ -443,6 +444,10 @@ GEM sys-uname (1.2.2) ffi (~> 1.1) table_print (1.5.7) + thin (1.8.1) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0, >= 1.0.4) + rack (>= 1, < 3) thor (1.2.1) tzinfo (2.0.4) concurrent-ruby (~> 1.0) diff --git a/app/domain/authentication/authn_k8s/authentication_request.rb b/app/domain/authentication/authn_k8s/authentication_request.rb index 53b1becc3e..63a3f343d9 100644 --- a/app/domain/authentication/authn_k8s/authentication_request.rb +++ b/app/domain/authentication/authn_k8s/authentication_request.rb @@ -16,8 +16,10 @@ def valid_restriction?(restriction) case restriction.name when Restrictions::NAMESPACE if restriction.value != @namespace - raise Errors::Authentication::AuthnK8s::NamespaceMismatch(@namespace, restriction.value) + raise Errors::Authentication::AuthnK8s::NamespaceMismatch.new(@namespace, restriction.value) end + when Restrictions::NAMESPACE_LABEL_SELECTOR + @k8s_resource_validator.valid_namespace?(label_selector: restriction.value) else # Restrictions defined using '-', but the k8s client expects type with '_' instead. # e.g. 'restriction=stateful-set' converted to 'k8s_type=stateful_set' diff --git a/app/domain/authentication/authn_k8s/consts.rb b/app/domain/authentication/authn_k8s/consts.rb index 7d6d307e82..0a8f1800b5 100644 --- a/app/domain/authentication/authn_k8s/consts.rb +++ b/app/domain/authentication/authn_k8s/consts.rb @@ -8,6 +8,7 @@ module AuthnK8s module Restrictions NAMESPACE = "namespace" + NAMESPACE_LABEL_SELECTOR = "namespace-label-selector" SERVICE_ACCOUNT = "service-account" POD = "pod" DEPLOYMENT = "deployment" @@ -17,13 +18,13 @@ module Restrictions # This is not exactly a restriction, because it only validates container existence and not requesting container name. AUTHENTICATION_CONTAINER_NAME = "authentication-container-name" - REQUIRED = [NAMESPACE].freeze + REQUIRED_EXCLUSIVE = [NAMESPACE, NAMESPACE_LABEL_SELECTOR].freeze RESOURCE_TYPE_EXCLUSIVE = [DEPLOYMENT, DEPLOYMENT_CONFIG, STATEFUL_SET].freeze OPTIONAL = [SERVICE_ACCOUNT, POD, AUTHENTICATION_CONTAINER_NAME].freeze - PERMITTED = REQUIRED + RESOURCE_TYPE_EXCLUSIVE + OPTIONAL + PERMITTED = REQUIRED_EXCLUSIVE + RESOURCE_TYPE_EXCLUSIVE + OPTIONAL CONSTRAINTS = Constraints::MultipleConstraint.new( - Constraints::RequiredConstraint.new(required: REQUIRED), + Constraints::RequiredExclusiveConstraint.new(required_exclusive: REQUIRED_EXCLUSIVE), Constraints::PermittedConstraint.new(permitted: PERMITTED), Constraints::ExclusiveConstraint.new(exclusive: RESOURCE_TYPE_EXCLUSIVE) ) diff --git a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb index a4dfe3fbe0..54b2655a4e 100644 --- a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb +++ b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb @@ -110,6 +110,12 @@ def pods_by_label(label_selector, namespace) k8s_client_for_method("get_pods").get_pods(label_selector: label_selector, namespace: namespace) end + # Locates namespace matching label selector in a namespace. + # + def namespace_by_label(namespace_name, label_selector) + k8s_client_for_method("get_namespaces").get_namespaces(field_selector: "metadata.name=#{namespace_name}", label_selector: label_selector) + end + # Look up an object according to the resource name. In Kubernetes, the # "resource" means something like ReplicaSet, Job, Deployment, etc. # diff --git a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb index 7b191fdc61..690e940b03 100644 --- a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb +++ b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb @@ -24,6 +24,13 @@ def valid_resource?(type:, name:) @logger.debug(LogMessages::Authentication::AuthnK8s::ValidatedK8sResource.new(type, name)) end + def valid_namespace?(label_selector:) + namespaces = @k8s_object_lookup.namespace_by_label(namespace, label_selector) + unless namespaces.any? + raise Errors::Authentication::AuthnK8s::NamespaceLabelSelectorMismatch.new(namespace, label_selector) + end + end + private def retrieve_k8s_resource(type, name) diff --git a/app/domain/authentication/constraints/required_exclusive_constraint.rb b/app/domain/authentication/constraints/required_exclusive_constraint.rb new file mode 100644 index 0000000000..4d0c99f206 --- /dev/null +++ b/app/domain/authentication/constraints/required_exclusive_constraint.rb @@ -0,0 +1,17 @@ +module Authentication + module Constraints + + class RequiredExclusiveConstraint + + def initialize(required_exclusive:) + @required_exclusive = required_exclusive + end + + def validate(resource_restrictions:) + restrictions_found = resource_restrictions & @required_exclusive + raise Errors::Authentication::Constraints::IllegalExclusiveRequiredCombination, @required_exclusive unless restrictions_found.length == 1 + end + + end + end +end diff --git a/app/domain/errors.rb b/app/domain/errors.rb index 484ad2e03a..c54d7c677e 100644 --- a/app/domain/errors.rb +++ b/app/domain/errors.rb @@ -289,6 +289,11 @@ module AuthnK8s code: "CONJ00026E" ) + NamespaceLabelSelectorMismatch = ::Util::TrackableErrorClass.new( + msg: "Kubernetes namespace '{0-namespace}' does not match label-selector: {1-label-selector}", + code: "CONJ00026E" + ) + ContainerNotFound = ::Util::TrackableErrorClass.new( msg: "Container '{0}' was not found in the pod. Host id: {1}", code: "CONJ00028E" @@ -690,6 +695,11 @@ module Constraints code: "CONJ00069E" ) + IllegalExclusiveRequiredCombination = ::Util::TrackableErrorClass.new( + msg: "Role must have only one of the following required constraints: {0-constraints}", + code: "CONJ00069E" + ) + NonPermittedRestrictionGiven = ::Util::TrackableErrorClass.new( msg: "Role can't have one of these none permitted restrictions '{0-restrictions}'", code: "CONJ00069E" diff --git a/spec/controllers/authn_k8s_test_server.rb b/spec/controllers/authn_k8s_test_server.rb new file mode 100644 index 0000000000..3cd3adbe9b --- /dev/null +++ b/spec/controllers/authn_k8s_test_server.rb @@ -0,0 +1,118 @@ +require 'rack' +require 'faye/websocket' +require 'pathname' + +Faye::WebSocket.load_adapter('thin') + +class AuthnK8sTestServer + attr_reader :copied_content + attr_reader :subpath + + def initialize(subpath) + @subpath = Pathname.new(subpath).to_s + end + + def self.run(...) + # TODO: find out how to get random ports + Rack::Handler::Thin.run self.new(...), :Port => 1234, :Host => "0.0.0.0" do |server| + puts "server running with port=#{server.port}, @subpath=#{server.app.subpath}" + + end + end + + def self.run_async(...) + Thread.new do + self.run(...) + end + end + + def ws_call(env) + ws = Faye::WebSocket.new(env) + r = Rack::Request.new(env) + + stdin = r.params["stdin"] == "true" + + ws.on :open do |event| + p [:open] + next if stdin + + ws.send("well then :)".bytes.unshift(1)) + ws.close(1000) + end + + ws.on :message do |event| + type, *message_bytes = event.data.bytes + message = message_bytes.pack('c*') + p [:message, type, message] + + path = "AAAA" + tmp_cert = "AAAA" + log_file = "AAAA" + content = "AAAA" + mode = "AAAA" + + escaped = Regexp.escape <<~BASH_SCRIPT + #!/bin/sh + set -e + + cleanup() { + rm -f "#{tmp_cert}" + } + trap cleanup EXIT + + set_file_content() { + cat > "#{tmp_cert}" < "#{log_file}" 2>&1 + BASH_SCRIPT + escaped = escaped.gsub("AAAA", "(.*)") + + + @copied_content = message.match(Regexp.new(escaped)).captures[2] + p [:cert, @copied_content] + ws.close(1000) + end + + + ws.on :close do |event| + p [:close, event.code, event.reason] + ws = nil + end + + # Return async Rack response + ws.rack_response + end + + def call(env) + req = Rack::Request.new(env) + puts "[authn-k8s test server] Handling request: #{req.request_method} path=#{req.fullpath}" + + return ws_call(env) if Faye::WebSocket.websocket?(env) + + if req.fullpath == "#{subpath}/api/v1" + [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:api.v1.json")] ] + elsif req.fullpath == "#{subpath}/api" + [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:api.json")] ] + elsif req.fullpath == "#{subpath}/apis" + [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:apis.json")] ] + elsif req.fullpath.start_with?("#{subpath}/apis/") + [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:apis.all.json")] ] + elsif req.fullpath == "#{subpath}/api/v1/namespaces/default/pods/bash-8449b79d7-c2fwd" + [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:api.v1.getpod.json")] ] + elsif req.fullpath == "#{subpath}/api/v1/namespaces?labelSelector=field.cattle.io%2FprojectId%3Dp-q7s7z&fieldSelector=metadata.name%3Ddefault" + [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:api.v1.getnamespaces.json")] ] + elsif req.fullpath.start_with?("#{subpath}/api/v1/namespaces?") + [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/bad:api.v1.getnamespaces.json")] ] + elsif req.fullpath == "#{subpath}/unauthorized" + [ 401, {"Content-Type" => "application/json"}, [File.read("./unauthorized.json")] ] + else + [ 200, {'Content-Type' => "application/json"}, [''] ] + end + end +end diff --git a/spec/controllers/bad:api.v1.getnamespaces.json b/spec/controllers/bad:api.v1.getnamespaces.json new file mode 100644 index 0000000000..16f405ca99 --- /dev/null +++ b/spec/controllers/bad:api.v1.getnamespaces.json @@ -0,0 +1,8 @@ +{ + "kind": "NamespaceList", + "apiVersion": "v1", + "metadata": { + "resourceVersion": "2445300" + }, + "items": [] +} diff --git a/spec/controllers/good:api.json b/spec/controllers/good:api.json new file mode 100644 index 0000000000..2282062b73 --- /dev/null +++ b/spec/controllers/good:api.json @@ -0,0 +1,12 @@ +{ + "kind": "APIVersions", + "versions": [ + "v1" + ], + "serverAddressByClientCIDRs": [ + { + "clientCIDR": "0.0.0.0/0", + "serverAddress": "10.0.147.71:6443" + } + ] +} diff --git a/spec/controllers/good:api.v1.getnamespaces.json b/spec/controllers/good:api.v1.getnamespaces.json new file mode 100644 index 0000000000..4be3a4a0e1 --- /dev/null +++ b/spec/controllers/good:api.v1.getnamespaces.json @@ -0,0 +1,89 @@ +{ + "kind": "NamespaceList", + "apiVersion": "v1", + "metadata": { + "resourceVersion": "2445300" + }, + "items": [ + { + "metadata": { + "name": "default", + "uid": "da93666e-c98d-44eb-9b04-eb59f2ccc000", + "resourceVersion": "1254", + "creationTimestamp": "2022-05-12T13:54:02Z", + "labels": { + "field.cattle.io/projectId": "p-q7s7z", + "kubernetes.io/metadata.name": "default" + }, + "annotations": { + "cattle.io/status": "{\"Conditions\":[{\"Type\":\"ResourceQuotaInit\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:26Z\"},{\"Type\":\"InitialRolesPopulated\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:31Z\"}]}", + "field.cattle.io/projectId": "c-4hbzx:p-q7s7z", + "lifecycle.cattle.io/create.namespace-auth": "true" + }, + "finalizers": [ + "controller.cattle.io/namespace-auth" + ], + "managedFields": [ + { + "manager": "kube-apiserver", + "operation": "Update", + "apiVersion": "v1", + "time": "2022-05-12T13:54:02Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + } + }, + { + "manager": "rancher", + "operation": "Update", + "apiVersion": "v1", + "time": "2022-05-12T13:55:25Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:cattle.io/status": {}, + "f:field.cattle.io/projectId": {}, + "f:lifecycle.cattle.io/create.namespace-auth": {} + }, + "f:finalizers": { + ".": {}, + "v:\"controller.cattle.io/namespace-auth\"": {} + } + } + } + }, + { + "manager": "agent", + "operation": "Update", + "apiVersion": "v1", + "time": "2022-05-12T13:55:49Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:field.cattle.io/projectId": {} + } + } + } + } + ] + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } + } + ] +} diff --git a/spec/controllers/good:api.v1.getpod.json b/spec/controllers/good:api.v1.getpod.json new file mode 100644 index 0000000000..6ff969bfa8 --- /dev/null +++ b/spec/controllers/good:api.v1.getpod.json @@ -0,0 +1,420 @@ +{ + "kind":"Pod", + "apiVersion":"v1", + "metadata":{ + "name":"bash-8449b79d7-c2fwd", + "generateName":"bash-8449b79d7-", + "namespace":"default", + "uid":"d68bded9-3b9b-419e-baaf-1d43a74454d3", + "resourceVersion":"128550906", + "creationTimestamp":"2022-02-14T16:32:56Z", + "labels":{ + "pod-template-hash":"8449b79d7", + "run":"bash" + }, + "annotations":{ + "k8s.v1.cni.cncf.io/network-status":"[{\n \"name\": \"openshift-sdn\",\n \"interface\": \"eth0\",\n \"ips\": [\n \"10.129.2.18\"\n ],\n \"default\": true,\n \"dns\": {}\n}]", + "k8s.v1.cni.cncf.io/networks-status":"[{\n \"name\": \"openshift-sdn\",\n \"interface\": \"eth0\",\n \"ips\": [\n \"10.129.2.18\"\n ],\n \"default\": true,\n \"dns\": {}\n}]" + }, + "ownerReferences":[ + { + "apiVersion":"apps/v1", + "kind":"ReplicaSet", + "name":"bash-8449b79d7", + "uid":"6185eb73-a1fb-43b2-b1a5-1ad15b24dbe4", + "controller":true, + "blockOwnerDeletion":true + } + ], + "managedFields":[ + { + "manager":"kube-controller-manager", + "operation":"Update", + "apiVersion":"v1", + "time":"2022-02-14T16:32:56Z", + "fieldsType":"FieldsV1", + "fieldsV1":{ + "f:metadata":{ + "f:generateName":{ + + }, + "f:labels":{ + ".":{ + + }, + "f:pod-template-hash":{ + + }, + "f:run":{ + + } + }, + "f:ownerReferences":{ + ".":{ + + }, + "k:{\"uid\":\"6185eb73-a1fb-43b2-b1a5-1ad15b24dbe4\"}":{ + ".":{ + + }, + "f:apiVersion":{ + + }, + "f:blockOwnerDeletion":{ + + }, + "f:controller":{ + + }, + "f:kind":{ + + }, + "f:name":{ + + }, + "f:uid":{ + + } + } + } + }, + "f:spec":{ + "f:containers":{ + "k:{\"name\":\"bash\"}":{ + ".":{ + + }, + "f:image":{ + + }, + "f:imagePullPolicy":{ + + }, + "f:name":{ + + }, + "f:resources":{ + + }, + "f:stdin":{ + + }, + "f:terminationMessagePath":{ + + }, + "f:terminationMessagePolicy":{ + + }, + "f:tty":{ + + } + } + }, + "f:dnsPolicy":{ + + }, + "f:enableServiceLinks":{ + + }, + "f:restartPolicy":{ + + }, + "f:schedulerName":{ + + }, + "f:securityContext":{ + + }, + "f:terminationGracePeriodSeconds":{ + + } + } + } + }, + { + "manager":"multus", + "operation":"Update", + "apiVersion":"v1", + "time":"2022-02-14T16:32:59Z", + "fieldsType":"FieldsV1", + "fieldsV1":{ + "f:metadata":{ + "f:annotations":{ + ".":{ + + }, + "f:k8s.v1.cni.cncf.io/network-status":{ + + }, + "f:k8s.v1.cni.cncf.io/networks-status":{ + + } + } + } + } + }, + { + "manager":"kubelet", + "operation":"Update", + "apiVersion":"v1", + "time":"2022-02-14T16:33:12Z", + "fieldsType":"FieldsV1", + "fieldsV1":{ + "f:status":{ + "f:conditions":{ + "k:{\"type\":\"ContainersReady\"}":{ + ".":{ + + }, + "f:lastProbeTime":{ + + }, + "f:lastTransitionTime":{ + + }, + "f:status":{ + + }, + "f:type":{ + + } + }, + "k:{\"type\":\"Initialized\"}":{ + ".":{ + + }, + "f:lastProbeTime":{ + + }, + "f:lastTransitionTime":{ + + }, + "f:status":{ + + }, + "f:type":{ + + } + }, + "k:{\"type\":\"Ready\"}":{ + ".":{ + + }, + "f:lastProbeTime":{ + + }, + "f:lastTransitionTime":{ + + }, + "f:status":{ + + }, + "f:type":{ + + } + } + }, + "f:containerStatuses":{ + + }, + "f:hostIP":{ + + }, + "f:phase":{ + + }, + "f:podIP":{ + + }, + "f:podIPs":{ + ".":{ + + }, + "k:{\"ip\":\"10.129.2.18\"}":{ + ".":{ + + }, + "f:ip":{ + + } + } + }, + "f:startTime":{ + + } + } + } + } + ] + }, + "spec":{ + "volumes":[ + { + "name":"kube-api-access-p4wqn", + "projected":{ + "sources":[ + { + "serviceAccountToken":{ + "expirationSeconds":3607, + "path":"token" + } + }, + { + "configMap":{ + "name":"kube-root-ca.crt", + "items":[ + { + "key":"ca.crt", + "path":"ca.crt" + } + ] + } + }, + { + "downwardAPI":{ + "items":[ + { + "path":"namespace", + "fieldRef":{ + "apiVersion":"v1", + "fieldPath":"metadata.namespace" + } + } + ] + } + }, + { + "configMap":{ + "name":"openshift-service-ca.crt", + "items":[ + { + "key":"service-ca.crt", + "path":"service-ca.crt" + } + ] + } + } + ], + "defaultMode":420 + } + } + ], + "containers":[ + { + "name":"bash", + "image":"debian", + "resources":{ + + }, + "volumeMounts":[ + { + "name":"kube-api-access-p4wqn", + "readOnly":true, + "mountPath":"/var/run/secrets/kubernetes.io/serviceaccount" + } + ], + "terminationMessagePath":"/dev/termination-log", + "terminationMessagePolicy":"File", + "imagePullPolicy":"Always", + "stdin":true, + "tty":true + } + ], + "restartPolicy":"Always", + "terminationGracePeriodSeconds":30, + "dnsPolicy":"ClusterFirst", + "serviceAccountName":"default", + "serviceAccount":"default", + "nodeName":"ip-10-0-136-67.ec2.internal", + "securityContext":{ + + }, + "imagePullSecrets":[ + { + "name":"default-dockercfg-nxkrg" + }, + { + "name":"dockerhub" + }, + { + "name":"dockerpullsecret" + } + ], + "schedulerName":"default-scheduler", + "tolerations":[ + { + "key":"node.kubernetes.io/not-ready", + "operator":"Exists", + "effect":"NoExecute", + "tolerationSeconds":300 + }, + { + "key":"node.kubernetes.io/unreachable", + "operator":"Exists", + "effect":"NoExecute", + "tolerationSeconds":300 + } + ], + "priority":0, + "enableServiceLinks":true, + "preemptionPolicy":"PreemptLowerPriority" + }, + "status":{ + "phase":"Running", + "conditions":[ + { + "type":"Initialized", + "status":"True", + "lastProbeTime":null, + "lastTransitionTime":"2022-02-14T16:32:56Z" + }, + { + "type":"Ready", + "status":"True", + "lastProbeTime":null, + "lastTransitionTime":"2022-02-14T16:33:12Z" + }, + { + "type":"ContainersReady", + "status":"True", + "lastProbeTime":null, + "lastTransitionTime":"2022-02-14T16:33:12Z" + }, + { + "type":"PodScheduled", + "status":"True", + "lastProbeTime":null, + "lastTransitionTime":"2022-02-14T16:32:56Z" + } + ], + "hostIP":"10.0.136.67", + "podIP":"10.129.2.18", + "podIPs":[ + { + "ip":"10.129.2.18" + } + ], + "startTime":"2022-02-14T16:32:56Z", + "containerStatuses":[ + { + "name":"bash", + "state":{ + "running":{ + "startedAt":"2022-02-14T16:33:11Z" + } + }, + "lastState":{ + + }, + "ready":true, + "restartCount":0, + "image":"docker.io/library/debian:latest", + "imageID":"docker.io/library/debian@sha256:7d8264bf731fec57d807d1918bec0a16550f52a9766f0034b40f55c5b7dc3712", + "containerID":"cri-o://d0a3ea365223486d0c62a9a8ce4657debc346f390e44361aa3dbf40cc2a3d539", + "started":true + } + ], + "qosClass":"BestEffort" + } + } + \ No newline at end of file diff --git a/spec/controllers/good:api.v1.json b/spec/controllers/good:api.v1.json new file mode 100644 index 0000000000..13e4750938 --- /dev/null +++ b/spec/controllers/good:api.v1.json @@ -0,0 +1,548 @@ +{ + "kind": "APIResourceList", + "groupVersion": "v1", + "resources": [ + { + "name": "bindings", + "singularName": "", + "namespaced": true, + "kind": "Binding", + "verbs": [ + "create" + ] + }, + { + "name": "componentstatuses", + "singularName": "", + "namespaced": false, + "kind": "ComponentStatus", + "verbs": [ + "get", + "list" + ], + "shortNames": [ + "cs" + ] + }, + { + "name": "configmaps", + "singularName": "", + "namespaced": true, + "kind": "ConfigMap", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "cm" + ], + "storageVersionHash": "qFsyl6wFWjQ=" + }, + { + "name": "endpoints", + "singularName": "", + "namespaced": true, + "kind": "Endpoints", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "ep" + ], + "storageVersionHash": "fWeeMqaN/OA=" + }, + { + "name": "events", + "singularName": "", + "namespaced": true, + "kind": "Event", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "ev" + ], + "storageVersionHash": "r2yiGXH7wu8=" + }, + { + "name": "limitranges", + "singularName": "", + "namespaced": true, + "kind": "LimitRange", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "limits" + ], + "storageVersionHash": "EBKMFVe6cwo=" + }, + { + "name": "namespaces", + "singularName": "", + "namespaced": false, + "kind": "Namespace", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "ns" + ], + "storageVersionHash": "Q3oi5N2YM8M=" + }, + { + "name": "namespaces/finalize", + "singularName": "", + "namespaced": false, + "kind": "Namespace", + "verbs": [ + "update" + ] + }, + { + "name": "namespaces/status", + "singularName": "", + "namespaced": false, + "kind": "Namespace", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "nodes", + "singularName": "", + "namespaced": false, + "kind": "Node", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "no" + ], + "storageVersionHash": "XwShjMxG9Fs=" + }, + { + "name": "nodes/proxy", + "singularName": "", + "namespaced": false, + "kind": "NodeProxyOptions", + "verbs": [ + "create", + "delete", + "get", + "patch", + "update" + ] + }, + { + "name": "nodes/status", + "singularName": "", + "namespaced": false, + "kind": "Node", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "persistentvolumeclaims", + "singularName": "", + "namespaced": true, + "kind": "PersistentVolumeClaim", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "pvc" + ], + "storageVersionHash": "QWTyNDq0dC4=" + }, + { + "name": "persistentvolumeclaims/status", + "singularName": "", + "namespaced": true, + "kind": "PersistentVolumeClaim", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "persistentvolumes", + "singularName": "", + "namespaced": false, + "kind": "PersistentVolume", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "pv" + ], + "storageVersionHash": "HN/zwEC+JgM=" + }, + { + "name": "persistentvolumes/status", + "singularName": "", + "namespaced": false, + "kind": "PersistentVolume", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "pods", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "po" + ], + "categories": [ + "all" + ], + "storageVersionHash": "xPOwRZ+Yhw8=" + }, + { + "name": "pods/attach", + "singularName": "", + "namespaced": true, + "kind": "PodAttachOptions", + "verbs": [ + "create", + "get" + ] + }, + { + "name": "pods/binding", + "singularName": "", + "namespaced": true, + "kind": "Binding", + "verbs": [ + "create" + ] + }, + { + "name": "pods/eviction", + "singularName": "", + "namespaced": true, + "group": "policy", + "version": "v1beta1", + "kind": "Eviction", + "verbs": [ + "create" + ] + }, + { + "name": "pods/exec", + "singularName": "", + "namespaced": true, + "kind": "PodExecOptions", + "verbs": [ + "create", + "get" + ] + }, + { + "name": "pods/log", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [ + "get" + ] + }, + { + "name": "pods/portforward", + "singularName": "", + "namespaced": true, + "kind": "PodPortForwardOptions", + "verbs": [ + "create", + "get" + ] + }, + { + "name": "pods/proxy", + "singularName": "", + "namespaced": true, + "kind": "PodProxyOptions", + "verbs": [ + "create", + "delete", + "get", + "patch", + "update" + ] + }, + { + "name": "pods/status", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "podtemplates", + "singularName": "", + "namespaced": true, + "kind": "PodTemplate", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "storageVersionHash": "LIXB2x4IFpk=" + }, + { + "name": "replicationcontrollers", + "singularName": "", + "namespaced": true, + "kind": "ReplicationController", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "rc" + ], + "categories": [ + "all" + ], + "storageVersionHash": "Jond2If31h0=" + }, + { + "name": "replicationcontrollers/scale", + "singularName": "", + "namespaced": true, + "group": "autoscaling", + "version": "v1", + "kind": "Scale", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "replicationcontrollers/status", + "singularName": "", + "namespaced": true, + "kind": "ReplicationController", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "resourcequotas", + "singularName": "", + "namespaced": true, + "kind": "ResourceQuota", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "quota" + ], + "storageVersionHash": "8uhSgffRX6w=" + }, + { + "name": "resourcequotas/status", + "singularName": "", + "namespaced": true, + "kind": "ResourceQuota", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "secrets", + "singularName": "", + "namespaced": true, + "kind": "Secret", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "storageVersionHash": "S6u1pOWzb84=" + }, + { + "name": "serviceaccounts", + "singularName": "", + "namespaced": true, + "kind": "ServiceAccount", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "sa" + ], + "storageVersionHash": "pbx9ZvyFpBE=" + }, + { + "name": "serviceaccounts/token", + "singularName": "", + "namespaced": true, + "group": "authentication.k8s.io", + "version": "v1", + "kind": "TokenRequest", + "verbs": [ + "create" + ] + }, + { + "name": "services", + "singularName": "", + "namespaced": true, + "kind": "Service", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "svc" + ], + "categories": [ + "all" + ], + "storageVersionHash": "0/CO1lhkEBI=" + }, + { + "name": "services/proxy", + "singularName": "", + "namespaced": true, + "kind": "ServiceProxyOptions", + "verbs": [ + "create", + "delete", + "get", + "patch", + "update" + ] + }, + { + "name": "services/status", + "singularName": "", + "namespaced": true, + "kind": "Service", + "verbs": [ + "get", + "patch", + "update" + ] + } + ] +} \ No newline at end of file diff --git a/spec/controllers/good:apis.all.json b/spec/controllers/good:apis.all.json new file mode 100644 index 0000000000..67ae6e68de --- /dev/null +++ b/spec/controllers/good:apis.all.json @@ -0,0 +1,41 @@ +{ + "kind": "APIResourceList", + "apiVersion": "v1", + "groupVersion": "autoscaling/v2beta2", + "resources": [ + { + "name": "horizontalpodautoscalers", + "singularName": "", + "namespaced": true, + "kind": "HorizontalPodAutoscaler", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "hpa" + ], + "categories": [ + "all" + ], + "storageVersionHash": "oQlkt7f5j/A=" + }, + { + "name": "horizontalpodautoscalers/status", + "singularName": "", + "namespaced": true, + "kind": "HorizontalPodAutoscaler", + "verbs": [ + "get", + "patch", + "update" + ] + } + ] +} diff --git a/spec/controllers/good:apis.json b/spec/controllers/good:apis.json new file mode 100644 index 0000000000..6500e9289a --- /dev/null +++ b/spec/controllers/good:apis.json @@ -0,0 +1,868 @@ +{ + "kind": "APIGroupList", + "apiVersion": "v1", + "groups": [ + { + "name": "apiregistration.k8s.io", + "versions": [ + { + "groupVersion": "apiregistration.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "apiregistration.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "apiregistration.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "apps", + "versions": [ + { + "groupVersion": "apps/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "apps/v1", + "version": "v1" + } + }, + { + "name": "events.k8s.io", + "versions": [ + { + "groupVersion": "events.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "events.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "events.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "authentication.k8s.io", + "versions": [ + { + "groupVersion": "authentication.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "authentication.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "authentication.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "authorization.k8s.io", + "versions": [ + { + "groupVersion": "authorization.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "authorization.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "authorization.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "autoscaling", + "versions": [ + { + "groupVersion": "autoscaling/v1", + "version": "v1" + }, + { + "groupVersion": "autoscaling/v2beta1", + "version": "v2beta1" + }, + { + "groupVersion": "autoscaling/v2beta2", + "version": "v2beta2" + } + ], + "preferredVersion": { + "groupVersion": "autoscaling/v1", + "version": "v1" + } + }, + { + "name": "batch", + "versions": [ + { + "groupVersion": "batch/v1", + "version": "v1" + }, + { + "groupVersion": "batch/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "batch/v1", + "version": "v1" + } + }, + { + "name": "certificates.k8s.io", + "versions": [ + { + "groupVersion": "certificates.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "certificates.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "certificates.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "networking.k8s.io", + "versions": [ + { + "groupVersion": "networking.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "networking.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "networking.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "extensions", + "versions": [ + { + "groupVersion": "extensions/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "extensions/v1beta1", + "version": "v1beta1" + } + }, + { + "name": "policy", + "versions": [ + { + "groupVersion": "policy/v1", + "version": "v1" + }, + { + "groupVersion": "policy/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "policy/v1", + "version": "v1" + } + }, + { + "name": "rbac.authorization.k8s.io", + "versions": [ + { + "groupVersion": "rbac.authorization.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "rbac.authorization.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "rbac.authorization.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "storage.k8s.io", + "versions": [ + { + "groupVersion": "storage.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "storage.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "storage.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "admissionregistration.k8s.io", + "versions": [ + { + "groupVersion": "admissionregistration.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "admissionregistration.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "admissionregistration.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "apiextensions.k8s.io", + "versions": [ + { + "groupVersion": "apiextensions.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "apiextensions.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "apiextensions.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "scheduling.k8s.io", + "versions": [ + { + "groupVersion": "scheduling.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "scheduling.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "scheduling.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "coordination.k8s.io", + "versions": [ + { + "groupVersion": "coordination.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "coordination.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "coordination.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "node.k8s.io", + "versions": [ + { + "groupVersion": "node.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "node.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "node.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "discovery.k8s.io", + "versions": [ + { + "groupVersion": "discovery.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "discovery.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "discovery.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "flowcontrol.apiserver.k8s.io", + "versions": [ + { + "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta1", + "version": "v1beta1" + } + }, + { + "name": "apps.openshift.io", + "versions": [ + { + "groupVersion": "apps.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "apps.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "authorization.openshift.io", + "versions": [ + { + "groupVersion": "authorization.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "authorization.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "build.openshift.io", + "versions": [ + { + "groupVersion": "build.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "build.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "image.openshift.io", + "versions": [ + { + "groupVersion": "image.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "image.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "oauth.openshift.io", + "versions": [ + { + "groupVersion": "oauth.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "oauth.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "project.openshift.io", + "versions": [ + { + "groupVersion": "project.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "project.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "quota.openshift.io", + "versions": [ + { + "groupVersion": "quota.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "quota.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "route.openshift.io", + "versions": [ + { + "groupVersion": "route.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "route.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "security.openshift.io", + "versions": [ + { + "groupVersion": "security.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "security.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "template.openshift.io", + "versions": [ + { + "groupVersion": "template.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "template.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "user.openshift.io", + "versions": [ + { + "groupVersion": "user.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "user.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "packages.operators.coreos.com", + "versions": [ + { + "groupVersion": "packages.operators.coreos.com/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "packages.operators.coreos.com/v1", + "version": "v1" + } + }, + { + "name": "config.openshift.io", + "versions": [ + { + "groupVersion": "config.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "config.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "operator.openshift.io", + "versions": [ + { + "groupVersion": "operator.openshift.io/v1", + "version": "v1" + }, + { + "groupVersion": "operator.openshift.io/v1alpha1", + "version": "v1alpha1" + } + ], + "preferredVersion": { + "groupVersion": "operator.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "apiserver.openshift.io", + "versions": [ + { + "groupVersion": "apiserver.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "apiserver.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "autoscaling.openshift.io", + "versions": [ + { + "groupVersion": "autoscaling.openshift.io/v1", + "version": "v1" + }, + { + "groupVersion": "autoscaling.openshift.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "autoscaling.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "cloudcredential.openshift.io", + "versions": [ + { + "groupVersion": "cloudcredential.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "cloudcredential.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "conjur.cyberark.com", + "versions": [ + { + "groupVersion": "conjur.cyberark.com/v1", + "version": "v1" + }, + { + "groupVersion": "conjur.cyberark.com/v1alpha1", + "version": "v1alpha1" + } + ], + "preferredVersion": { + "groupVersion": "conjur.cyberark.com/v1", + "version": "v1" + } + }, + { + "name": "console.openshift.io", + "versions": [ + { + "groupVersion": "console.openshift.io/v1", + "version": "v1" + }, + { + "groupVersion": "console.openshift.io/v1alpha1", + "version": "v1alpha1" + } + ], + "preferredVersion": { + "groupVersion": "console.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "imageregistry.operator.openshift.io", + "versions": [ + { + "groupVersion": "imageregistry.operator.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "imageregistry.operator.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "ingress.operator.openshift.io", + "versions": [ + { + "groupVersion": "ingress.operator.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "ingress.operator.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "k8s.cni.cncf.io", + "versions": [ + { + "groupVersion": "k8s.cni.cncf.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "k8s.cni.cncf.io/v1", + "version": "v1" + } + }, + { + "name": "machineconfiguration.openshift.io", + "versions": [ + { + "groupVersion": "machineconfiguration.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "machineconfiguration.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "monitoring.coreos.com", + "versions": [ + { + "groupVersion": "monitoring.coreos.com/v1", + "version": "v1" + }, + { + "groupVersion": "monitoring.coreos.com/v1alpha1", + "version": "v1alpha1" + } + ], + "preferredVersion": { + "groupVersion": "monitoring.coreos.com/v1", + "version": "v1" + } + }, + { + "name": "network.openshift.io", + "versions": [ + { + "groupVersion": "network.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "network.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "network.operator.openshift.io", + "versions": [ + { + "groupVersion": "network.operator.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "network.operator.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "operators.coreos.com", + "versions": [ + { + "groupVersion": "operators.coreos.com/v2", + "version": "v2" + }, + { + "groupVersion": "operators.coreos.com/v1", + "version": "v1" + }, + { + "groupVersion": "operators.coreos.com/v1alpha2", + "version": "v1alpha2" + }, + { + "groupVersion": "operators.coreos.com/v1alpha1", + "version": "v1alpha1" + } + ], + "preferredVersion": { + "groupVersion": "operators.coreos.com/v2", + "version": "v2" + } + }, + { + "name": "samples.operator.openshift.io", + "versions": [ + { + "groupVersion": "samples.operator.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "samples.operator.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "security.internal.openshift.io", + "versions": [ + { + "groupVersion": "security.internal.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "security.internal.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "snapshot.storage.k8s.io", + "versions": [ + { + "groupVersion": "snapshot.storage.k8s.io/v1", + "version": "v1" + }, + { + "groupVersion": "snapshot.storage.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "snapshot.storage.k8s.io/v1", + "version": "v1" + } + }, + { + "name": "tuned.openshift.io", + "versions": [ + { + "groupVersion": "tuned.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "tuned.openshift.io/v1", + "version": "v1" + } + }, + { + "name": "controlplane.operator.openshift.io", + "versions": [ + { + "groupVersion": "controlplane.operator.openshift.io/v1alpha1", + "version": "v1alpha1" + } + ], + "preferredVersion": { + "groupVersion": "controlplane.operator.openshift.io/v1alpha1", + "version": "v1alpha1" + } + }, + { + "name": "metal3.io", + "versions": [ + { + "groupVersion": "metal3.io/v1alpha1", + "version": "v1alpha1" + } + ], + "preferredVersion": { + "groupVersion": "metal3.io/v1alpha1", + "version": "v1alpha1" + } + }, + { + "name": "migration.k8s.io", + "versions": [ + { + "groupVersion": "migration.k8s.io/v1alpha1", + "version": "v1alpha1" + } + ], + "preferredVersion": { + "groupVersion": "migration.k8s.io/v1alpha1", + "version": "v1alpha1" + } + }, + { + "name": "whereabouts.cni.cncf.io", + "versions": [ + { + "groupVersion": "whereabouts.cni.cncf.io/v1alpha1", + "version": "v1alpha1" + } + ], + "preferredVersion": { + "groupVersion": "whereabouts.cni.cncf.io/v1alpha1", + "version": "v1alpha1" + } + }, + { + "name": "helm.openshift.io", + "versions": [ + { + "groupVersion": "helm.openshift.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "helm.openshift.io/v1beta1", + "version": "v1beta1" + } + }, + { + "name": "machine.openshift.io", + "versions": [ + { + "groupVersion": "machine.openshift.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "machine.openshift.io/v1beta1", + "version": "v1beta1" + } + }, + { + "name": "metrics.k8s.io", + "versions": [ + { + "groupVersion": "metrics.k8s.io/v1beta1", + "version": "v1beta1" + } + ], + "preferredVersion": { + "groupVersion": "metrics.k8s.io/v1beta1", + "version": "v1beta1" + } + } + ] +} diff --git a/spec/controllers/unauthorized.json b/spec/controllers/unauthorized.json new file mode 100644 index 0000000000..06f1caeeaa --- /dev/null +++ b/spec/controllers/unauthorized.json @@ -0,0 +1,9 @@ +{ + "kind":"Status", + "apiVersion":"v1", + "metadata":{}, + "status":"Failure", + "message":"Unauthorized", + "reason":"Unauthorized", + "code":401 + } From 9553766a5f1cef5c63bfdf0a663889d336c43bab Mon Sep 17 00:00:00 2001 From: Kumbirai Tanekha Date: Tue, 28 Jun 2022 22:10:45 +0000 Subject: [PATCH 02/14] Build and push eval appliance image --- Dockerfile.appliance | 10 + Jenkinsfile | 596 +----------------------- build-and-publish-internal-appliance.sh | 8 + 3 files changed, 20 insertions(+), 594 deletions(-) create mode 100644 Dockerfile.appliance create mode 100755 build-and-publish-internal-appliance.sh diff --git a/Dockerfile.appliance b/Dockerfile.appliance new file mode 100644 index 0000000000..167d0498ee --- /dev/null +++ b/Dockerfile.appliance @@ -0,0 +1,10 @@ +FROM registry.tld/conjur-appliance:5.0-stable + +COPY app/domain/authentication/authn_k8s/authentication_request.rb /opt/conjur/possum/app/domain/authentication/authn_k8s/authentication_request.rb +COPY app/domain/authentication/authn_k8s/consts.rb /opt/conjur/possum/app/app/domain/authentication/authn_k8s/consts.rb +COPY app/domain/authentication/authn_k8s/k8s_object_lookup.rb /opt/conjur/possum/app/app/domain/authentication/authn_k8s/k8s_object_lookup.rb +COPY app/domain/authentication/authn_k8s/k8s_resource_validator.rb /opt/conjur/possum/app/app/domain/authentication/authn_k8s/k8s_resource_validator.rb +COPY app/domain/authentication/constraints/required_exclusive_constraint.rb /opt/conjur/possum/app/app/domain/authentication/constraints/required_exclusive_constraint.rb +COPY app/domain/errors.rb /opt/conjur/possum/app/app/domain/errors.rb + +RUN chown -R conjur:conjur /opt/conjur/possum/app diff --git a/Jenkinsfile b/Jenkinsfile index bcd6ca194e..3d36f3cbaa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -123,603 +123,11 @@ pipeline { } stages { - // Aborts any builds triggered by another project that wouldn't include any changes - stage ("Skip build if triggering job didn't create a release") { - when { - expression { - MODE == "SKIP" - } - } - steps { - script { - currentBuild.result = 'ABORTED' - error("Aborting build because this build was triggered from upstream, but no release was built") - } - } - } - // Generates a VERSION file based on the current build number and latest version in CHANGELOG.md - stage('Validate Changelog and set version') { - steps { - updateVersion("CHANGELOG.md", "${BUILD_NUMBER}") - stash name: 'version_info', includes: 'VERSION' - } - } - - stage('Fetch tags') { - steps { - withCredentials( - [ - usernameColonPassword( - credentialsId: 'conjur-jenkins-api', variable: 'GITCREDS' - ) - ] - ) { - sh ''' - git fetch --tags "$( - git remote get-url origin | - sed -e "s|https://|https://$GITCREDS@|" - )" - # print them out to make sure, can remove when this is robust - git tag - ''' - } - } - } - - stage('Validate Changelog') { - when { - expression { params.RUN_ONLY == '' } - } - steps { - sh 'ci/parse-changelog' - } - } - - stage('Build and test Conjur') { - when { - // Run tests only when ANY of the following is true: - // 1. A non-markdown file has changed. - // 2. It's running on the master branch (which includes nightly builds). - // 3. It's a tag-triggered build. - anyOf { - // Note: You cannot use "when"'s changeset condition here because it's - // not powerful enough to express "_only_ md files have changed". - // Dropping down to a git script was the easiest alternative. - expression { - 0 == sh( - returnStatus: true, - // A non-markdown file has changed. - script: ''' - git diff origin/master --name-only | - grep -v "^.*\\.md$" > /dev/null - ''' - ) - } - - // Always run the full pipeline on the master branch (which includes - // nightly builds) - branch "master" - - // Always run the full pipeline on tags of the form v* - tag "v*" - } - } - - stages { - stage('Build Docker Image') { - steps { - sh './build.sh --jenkins' - } - } - - stage('Push images to internal registry') { - steps { - // Push images to the internal registry so that they can be used - // by tests, even if the tests run on a different executor. - sh './publish-images.sh --internal' - } - } - - stage('Scan Docker Image') { - when { - expression { params.RUN_ONLY == '' } - } - parallel { - stage("Scan Docker Image for fixable issues") { - steps { - scanAndReport("conjur:${tagWithSHA()}", "HIGH", false) - } - } - stage("Scan Docker image for total issues") { - steps { - scanAndReport("conjur:${tagWithSHA()}", "NONE", true) - } - } - stage("Scan UBI-based Docker Image for fixable issues") { - steps { - scanAndReport("conjur-ubi:${tagWithSHA()}", "HIGH", false) - } - } - stage("Scan UBI-based Docker image for total issues") { - steps { - scanAndReport("conjur-ubi:${tagWithSHA()}", "NONE", true) - } - } - } - } - - // TODO: Add comments explaining which env vars are set here. - stage('Prepare For CodeClimate Coverage Report Submission') { - when { - expression { params.RUN_ONLY == '' } - } - steps { - catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { - script { - ccCoverage.dockerPrep() - sh 'mkdir -p coverage' - env.CODE_CLIMATE_PREPARED = "true" - } - } - } - } - - // Run outside parallel block to reduce main Jenkins executor load. - stage('Nightly Only') { - when { - expression { params.NIGHTLY } - } - - environment { - CUCUMBER_FILTER_TAGS = "${params.CUCUMBER_FILTER_TAGS}" - } - - stages { - stage('EE FIPS agent tests') { - agent { label 'executor-v2-rhel-ee' } - - steps { - unstash 'version_info' - // Catch errors so remaining steps always run. - catchError { - runConjurTests(params.RUN_ONLY) - } - - stash( - name: 'testResultEE', - includes: ''' - cucumber/*/*.*, - container_logs/*/*, - spec/reports/*.xml, - spec/reports-audit/*.xml, - cucumber/*/features/reports/**/*.xml - ''' - ) - } - - post { - always { - dir('ee-test'){ - unstash 'testResultEE' - } - - archiveArtifacts( - artifacts: "ee-test/cucumber/*/*.*", - fingerprint: false, - allowEmptyArchive: true - ) - - archiveArtifacts( - artifacts: "ee-test/container_logs/*/*", - fingerprint: false, - allowEmptyArchive: true - ) - - publishHTML( - reportDir: 'ee-test/cucumber', - reportFiles: ''' - api/cucumber_results.html, - authenticators_config/cucumber_results.html, - authenticators_azure/cucumber_results.html, - authenticators_ldap/cucumber_results.html, - authenticators_oidc/cucumber_results.html, - authenticators_jwt/cucumber_results.html, - authenticators_status/cucumber_results.html - policy/cucumber_results.html, - rotators/cucumber_results.html - ''', - reportName: 'EE Integration reports', - reportTitles: '', - allowMissing: false, - alwaysLinkToLastBuild: true, - keepAll: true - ) - } - } - } - } - } - - stage('Run environment tests in parallel') { - parallel { - stage('Standard agent tests') { - environment { - CUCUMBER_FILTER_TAGS = "${params.CUCUMBER_FILTER_TAGS}" - } - - steps { - runConjurTests(params.RUN_ONLY) - } - } - - stage('Azure Authenticator') { - when { - expression { - testShouldRun(params.RUN_ONLY, "azure_authenticator") - } - } - - agent { label 'azure-linux' } - - environment { - // TODO: Move this into the authenticators_azure bash script. - AZURE_AUTHN_INSTANCE_IP = sh( - script: 'curl "http://checkip.amazonaws.com"', - returnStdout: true - ).trim() - // TODO: Move this into the authenticators_azure bash script. - SYSTEM_ASSIGNED_IDENTITY = sh( - script: 'ci/test_suites/authenticators_azure/' + - 'get_system_assigned_identity.sh', - returnStdout: true - ).trim() - } - - steps { - unstash 'version_info' - // Grant access to this Jenkins agent's IP to AWS security groups - // This is required for access to the internal docker registry - // from outside EC2. - grantIPAccess() - sh( - 'summon -f ci/test_suites/authenticators_azure/secrets.yml ' + - 'ci/test authenticators_azure' - ) - } - - post { - always { - stash( - name: 'testResultAzure', - allowEmpty: true, - includes: ''' - cucumber/*azure*/*.*, - container_logs/*azure*/*, - cucumber_results*.json - ''' - ) - // Remove this Agent's IP from IPManager's prefix list - // There are a limited number of entries, so it remove it - // rather than waiting for it to expire. - removeIPAccess() - } - } - } - /** - * GCP Authenticator -- Token Stashing -- Stage 1 of 3 - * - * In this stage, a GCE instance node is allocated, a script runs - * and retrieves all the tokens that will be used in authn-gcp - * tests. The token are stashed, and later un-stashed and used in - * the stage that runs the GCP Authenticator tests. This way we can - * have a light-weight GCE instance that has no dependency on - * conjurops or git identities and is not open for SSH. - */ - stage('GCP Authenticator preparation - Allocate GCE Instance') { - when { - expression { - testShouldRun(params.RUN_ONLY, "gcp_authenticator") - } - } - steps { - echo '-- Allocating Google Compute Engine' - - script { - dir('ci/test_suites/authenticators_gcp') { - stash( - name: 'get_gce_tokens_script', - includes: ''' - get_gce_tokens_to_files.sh, - get_tokens_to_files.sh, - tokens_config.json - ''' - ) - } - - node('executor-v2-gcp-small') { - echo '-- Google Compute Engine allocated' - echo '-- Get compute engine instance project name from ' + - 'Google metadata server.' - // TODO: Move this into get_gce_tokens_to_files.sh - env.GCP_PROJECT = sh( - script: 'curl -s -H "Metadata-Flavor: Google" ' + - '"http://metadata.google.internal/computeMetadata/v1/' + - 'project/project-id"', - returnStdout: true - ).trim() - unstash('get_gce_tokens_script') - sh('./get_gce_tokens_to_files.sh') - stash( - name: 'authnGceTokens', - includes: 'gce_token_*', - allowEmpty:false - ) - } - } - } - post { - failure { - script { - env.GCP_ENV_ERROR = "true" - } - } - success { - script { - env.GCE_TOKENS_FETCHED = "true" - } - echo '-- Finished fetching GCE tokens.' - } - } - } - - /** - * GCP Authenticator -- Allocate Function -- Stage 2 of 3 - * - * In this stage, Google SDK container executes a script to deploy a - * function, the function accepts audience in query string and - * returns a token with that audience. All the tokens required for - * testings are obtained and written to function directory, the post - * stage branch deletes the function. This stage depends on stage: - * 'GCP Authenticator preparation - Allocate GCE Instance' to set - * the GCP project env var. - */ - stage('GCP Authenticator preparation - Allocate Google Function') { - when { - expression { - testShouldRun(params.RUN_ONLY, "gcp_authenticator") - } - } - environment { - GCP_FETCH_TOKEN_FUNCTION = "fetch_token_${BUILD_NUMBER}" - IDENTITY_TOKEN_FILE = 'identity-token' - GCP_OWNER_SERVICE_KEY_FILE = "sa-key-file.json" - } - steps { - echo "Waiting for GCP project name (Set by stage: " + - "'GCP Authenticator preparation - Allocate GCE Instance')" - timeout(time: 10, unit: 'MINUTES') { - waitUntil { - script { - return ( - env.GCP_PROJECT != null || env.GCP_ENV_ERROR == "true" - ) - } - } - } - script { - if (env.GCP_ENV_ERROR == "true") { - error('GCP_ENV_ERROR cannot deploy function') - } - - dir('ci/test_suites/authenticators_gcp') { - sh('summon ./deploy_function_and_get_tokens.sh') - } - } - } - post { - success { - echo "-- Google Cloud test env is ready" - script { - env.GCP_FUNC_TOKENS_FETCHED = "true" - } - } - failure { - echo "-- GCP function deployment stage failed" - script { - env.GCP_ENV_ERROR = "true" - } - } - always { - script { - dir('ci/test_suites/authenticators_gcp') { - sh ''' - # Cleanup Google function - summon ./run_gcloud.sh cleanup_function.sh - ''' - } - } - } - } - } - /** - * GCP Authenticator -- Run Tests -- Stage 3 of 3 - * - * We have two preparation stages before running the GCP - * Authenticator tests stage. This stage waits for GCP preparation - * stages to complete, un-stashes the tokens created in stage: 'GCP - * Authenticator preparation - Allocate GCE Instance' and runs the - * gcp-authn tests. - */ - stage('GCP Authenticator - Run Tests') { - when { - expression { - testShouldRun(params.RUN_ONLY, "gcp_authenticator") - } - } - steps { - echo('Waiting for GCP Tokens provisioned by prep stages.') - - timeout(time: 10, unit: 'MINUTES') { - waitUntil { - script { - return ( - env.GCP_ENV_ERROR == "true" || - ( - env.GCP_FUNC_TOKENS_FETCHED == "true" && - env.GCE_TOKENS_FETCHED == "true" - ) - ) - } - } - } - script { - if (env.GCP_ENV_ERROR == "true") { - error( - 'GCP_ENV_ERROR: Check logs for errors in stages 1 and 2' - ) - } - } - script { - dir('ci/test_suites/authenticators_gcp/tokens') { - unstash 'authnGceTokens' - } - sh 'ci/test authenticators_gcp' - } - } - } - } - } - } - - post { - success { - script { - if (env.BRANCH_NAME == 'master') { - build( - job:'../cyberark--secrets-provider-for-k8s/main', - wait: false - ) - } - } - } - - always { - script { - - // Only unstash azure if it ran. - if (testShouldRun(params.RUN_ONLY, "azure_authenticator")) { - unstash 'testResultAzure' - } - - // Make files available for download. - archiveFiles('container_logs/*/*') - archiveFiles('coverage/.resultset*.json') - archiveFiles('coverage/coverage.json') - archiveFiles('coverage/codeclimate.json') - archiveFiles( - 'ci/test_suites/authenticators_k8s/output/simplecov-resultset-authnk8s-gke.json' - ) - archiveFiles('cucumber/*/*.*') - - publishHTML([ - reportName: 'Integration reports', - reportDir: 'cucumber', - reportFiles: ''' - api/cucumber_results.html, - authenticators_config/cucumber_results.html, - authenticators_azure/cucumber_results.html, - authenticators_ldap/cucumber_results.html, - authenticators_oidc/cucumber_results.html, - authenticators_jwt/cucumber_results.html, - authenticators_gcp/cucumber_results.html, - authenticators_status/cucumber_results.html, - authenticators_k8s/cucumber_results.html, - policy/cucumber_results.html, - rotators/cucumber_results.html - ''', - reportTitles: '', - allowMissing: false, - alwaysLinkToLastBuild: true, - keepAll: true - ]) - - publishHTML( - reportName: 'Coverage Report', - reportDir: 'coverage', - reportFiles: 'index.html', - reportTitles: '', - allowMissing: false, - alwaysLinkToLastBuild: true, - keepAll: true - ) - junit(''' - spec/reports/*.xml, - spec/reports-audit/*.xml, - cucumber/*/features/reports/**/*.xml, - ee-test/spec/reports/*.xml, - ee-test/spec/reports-audit/*.xml, - ee-test/cucumber/*/features/reports/**/*.xml - ''' - ) - - // Make cucumber reports available as html report in Jenkins UI. - cucumber( - fileIncludePattern: '**/cucumber_results.json', - sortingMethod: 'ALPHABETICAL' - ) - } - } - } - } // end stage: build and test conjur - - stage('Submit Coverage Report') { - when { - expression { - env.CODE_CLIMATE_PREPARED == "true" - } - } + stage('Build and publish internal appliance') { steps{ - sh 'ci/submit-coverage' + sh './build-and-publish-internal-appliance.sh' } } - - stage("Release Conjur images and packages") { - when { - expression { - MODE == "RELEASE" - } - } - steps { - release { billOfMaterialsDirectory, assetDirectory -> - // Publish docker images - sh './publish-images.sh --edge --dockerhub' - - // Create deb and rpm packages - sh 'echo "CONJUR_VERSION=5" >> debify.env' - sh './package.sh' - archiveArtifacts artifacts: '*.deb', fingerprint: true - archiveArtifacts artifacts: '*.rpm', fingerprint: true - sh "cp *.rpm ${assetDirectory}/." - sh "cp *.deb ${assetDirectory}/." - - // Publish deb and rpm packages - sh './publish.sh' - } - } - } - } - - post { - always { - // Explanation of arguments: - // cleanupAndNotify(buildStatus, slackChannel, additionalMessage, ticket) - cleanupAndNotify( - currentBuild.currentResult, - '#conjur-core', - "${(params.NIGHTLY ? 'nightly' : '')}", - true - ) - } } } diff --git a/build-and-publish-internal-appliance.sh b/build-and-publish-internal-appliance.sh new file mode 100755 index 0000000000..b16954c7ea --- /dev/null +++ b/build-and-publish-internal-appliance.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + + +IMAGE="registry.tld/conjur-appliance:eval-authn-k8s-label-selector" + +docker build -f ./Dockerfile.appliance -t "${IMAGE}" . +docker push "${IMAGE}" From 070f233ea01438d3fd8a598da803de238562e7ab Mon Sep 17 00:00:00 2001 From: Kumbirai Tanekha Date: Wed, 29 Jun 2022 10:23:14 +0100 Subject: [PATCH 03/14] Fix Dockerfile.appliance --- Dockerfile.appliance | 11 ++++++----- build-and-publish-internal-appliance.sh | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Dockerfile.appliance b/Dockerfile.appliance index 167d0498ee..9b7e786f29 100644 --- a/Dockerfile.appliance +++ b/Dockerfile.appliance @@ -1,10 +1,11 @@ FROM registry.tld/conjur-appliance:5.0-stable +# Copy new source files COPY app/domain/authentication/authn_k8s/authentication_request.rb /opt/conjur/possum/app/domain/authentication/authn_k8s/authentication_request.rb -COPY app/domain/authentication/authn_k8s/consts.rb /opt/conjur/possum/app/app/domain/authentication/authn_k8s/consts.rb -COPY app/domain/authentication/authn_k8s/k8s_object_lookup.rb /opt/conjur/possum/app/app/domain/authentication/authn_k8s/k8s_object_lookup.rb -COPY app/domain/authentication/authn_k8s/k8s_resource_validator.rb /opt/conjur/possum/app/app/domain/authentication/authn_k8s/k8s_resource_validator.rb -COPY app/domain/authentication/constraints/required_exclusive_constraint.rb /opt/conjur/possum/app/app/domain/authentication/constraints/required_exclusive_constraint.rb -COPY app/domain/errors.rb /opt/conjur/possum/app/app/domain/errors.rb +COPY app/domain/authentication/authn_k8s/consts.rb /opt/conjur/possum/app/domain/authentication/authn_k8s/consts.rb +COPY app/domain/authentication/authn_k8s/k8s_object_lookup.rb /opt/conjur/possum/app/domain/authentication/authn_k8s/k8s_object_lookup.rb +COPY app/domain/authentication/authn_k8s/k8s_resource_validator.rb /opt/conjur/possum/app/domain/authentication/authn_k8s/k8s_resource_validator.rb +COPY app/domain/authentication/constraints/required_exclusive_constraint.rb /opt/conjur/possum/app/domain/authentication/constraints/required_exclusive_constraint.rb +COPY app/domain/errors.rb /opt/conjur/possum/app/domain/errors.rb RUN chown -R conjur:conjur /opt/conjur/possum/app diff --git a/build-and-publish-internal-appliance.sh b/build-and-publish-internal-appliance.sh index b16954c7ea..b70654099d 100755 --- a/build-and-publish-internal-appliance.sh +++ b/build-and-publish-internal-appliance.sh @@ -4,5 +4,24 @@ set -euo pipefail IMAGE="registry.tld/conjur-appliance:eval-authn-k8s-label-selector" +echo "FROM registry.tld/conjur-appliance:5.0-stable + +# Copy new source files +$( + echo " +app/domain/authentication/authn_k8s/authentication_request.rb +app/domain/authentication/authn_k8s/consts.rb +app/domain/authentication/authn_k8s/k8s_object_lookup.rb +app/domain/authentication/authn_k8s/k8s_resource_validator.rb +app/domain/authentication/constraints/required_exclusive_constraint.rb +app/domain/errors.rb +" | docker run --rm -i --entrypoint="" ruby:2-alpine ruby -e ' +files = STDIN.read.split("\n").reject(&:empty?) +puts files.map {|file| "COPY #{file} /opt/conjur/possum/#{file}"}.join("\n") +' +) + +RUN chown -R conjur:conjur /opt/conjur/possum/app" > Dockerfile.appliance + docker build -f ./Dockerfile.appliance -t "${IMAGE}" . docker push "${IMAGE}" From 31b1f6f395721cd9c0d4132cafcd95517fde1b6a Mon Sep 17 00:00:00 2001 From: Kumbirai Tanekha Date: Wed, 29 Jun 2022 16:46:28 +0100 Subject: [PATCH 04/14] Example of what local label-selector logic might look like --- .../authn_k8s/k8s_object_lookup.rb | 19 +++++++++++++++++++ .../authn_k8s/k8s_resource_validator.rb | 19 +++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb index 54b2655a4e..48b4641f55 100644 --- a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb +++ b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb @@ -104,6 +104,13 @@ def pod_by_name(podname, namespace) k8s_client_for_method("get_pod").get_pod(podname, namespace) end + # Locates the Namespace with a given name. + # + # @return nil if no such Namespace exists. + def namespace_by_name(namespace) + k8s_client_for_method("get_namespace").get_namespace(namespace) + end + # Locates pods matching label selector in a namespace. # def pods_by_label(label_selector, namespace) @@ -221,3 +228,15 @@ def load_additional_certs(ssl_cert_directory) end end end + +# rails c +# +# webservice = ::Authentication::Webservice.new( +# account: "rancherDemoAccount", +# authenticator_name: "authn-k8s", +# service_id: "demo/rancher" +# ) + +# k8s_object_lookup = Authentication::AuthnK8s::K8sObjectLookup.new(webservice) +# namespace = k8s_object_lookup.kube_client.get_namespace("default") +# namespace.metadata.labels.to_h diff --git a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb index 690e940b03..1de4109d09 100644 --- a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb +++ b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb @@ -25,8 +25,23 @@ def valid_resource?(type:, name:) end def valid_namespace?(label_selector:) - namespaces = @k8s_object_lookup.namespace_by_label(namespace, label_selector) - unless namespaces.any? + # APPROACH + # + # namespaces = @k8s_object_lookup.namespace_by_label(namespace, label_selector) + # condition = namespaces.any? + + # APPROACH + namespace_object = @k8s_object_lookup.namespace_by_name(namespace) + + # in the spirit of https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go + labels_h = namespace_object.metadata.labels.to_h + label_selector_h = label_selector + .split(",") + .map{ |kv_pair| kv_pair = kv_pair.split("="); kv_pair[0] = kv_pair[0].to_sym; kv_pair } + .to_h + condition = label_selector_h.all? { |k, v| labels_h[k] == v } + + unless condition raise Errors::Authentication::AuthnK8s::NamespaceLabelSelectorMismatch.new(namespace, label_selector) end end From 62ba8043e013d8510f89ac2599b4b2a6be7c4c43 Mon Sep 17 00:00:00 2001 From: John ODonnell Date: Fri, 22 Jul 2022 10:13:14 -0400 Subject: [PATCH 05/14] Add unit tests for new XOR constraint --- .../required_exclusive_constraint.rb | 4 +- app/domain/errors.rb | 5 +- .../required_exclusive_constraint_spec.rb | 63 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 spec/app/domain/authentication/constraints/required_exclusive_constraint_spec.rb diff --git a/app/domain/authentication/constraints/required_exclusive_constraint.rb b/app/domain/authentication/constraints/required_exclusive_constraint.rb index 4d0c99f206..aaf0a2ef83 100644 --- a/app/domain/authentication/constraints/required_exclusive_constraint.rb +++ b/app/domain/authentication/constraints/required_exclusive_constraint.rb @@ -1,6 +1,8 @@ module Authentication module Constraints + # This constraint is initialized with an array of strings. + # They represent resource restrictions where exactly one is required. class RequiredExclusiveConstraint def initialize(required_exclusive:) @@ -9,7 +11,7 @@ def initialize(required_exclusive:) def validate(resource_restrictions:) restrictions_found = resource_restrictions & @required_exclusive - raise Errors::Authentication::Constraints::IllegalExclusiveRequiredCombination, @required_exclusive unless restrictions_found.length == 1 + raise Errors::Authentication::Constraints::IllegalRequiredExclusiveCombination.new(@required_exclusive, restrictions_found) unless restrictions_found.length == 1 end end diff --git a/app/domain/errors.rb b/app/domain/errors.rb index c54d7c677e..7e74ed7626 100644 --- a/app/domain/errors.rb +++ b/app/domain/errors.rb @@ -695,8 +695,9 @@ module Constraints code: "CONJ00069E" ) - IllegalExclusiveRequiredCombination = ::Util::TrackableErrorClass.new( - msg: "Role must have only one of the following required constraints: {0-constraints}", + IllegalRequiredExclusiveCombination = ::Util::TrackableErrorClass.new( + msg: "Role must have exactly one of the following required constraints: " \ + "{0-constraints}. Role configured with {1-provided}", code: "CONJ00069E" ) diff --git a/spec/app/domain/authentication/constraints/required_exclusive_constraint_spec.rb b/spec/app/domain/authentication/constraints/required_exclusive_constraint_spec.rb new file mode 100644 index 0000000000..cec2c6ed6b --- /dev/null +++ b/spec/app/domain/authentication/constraints/required_exclusive_constraint_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe(Authentication::Constraints::RequiredExclusiveConstraint) do + context "Given RequiredExclusiveConstraint initialized with 3 restrictions" do + let(:reqx_restrictions) { %w[reqx_one reqx_two reqx_three] } + let(:additional_restriction) { "additional" } + let(:raised_error) { ::Errors::Authentication::Constraints::IllegalRequiredExclusiveCombination } + + subject(:constraint) do + Authentication::Constraints::RequiredExclusiveConstraint.new(required_exclusive: reqx_restrictions) + end + + context "when validating with no ReqX restrictions" do + let(:expected_error_message) { /#{Regexp.escape(reqx_restrictions.to_s)}/ } + + subject do + constraint.validate(resource_restrictions: [additional_restriction]) + end + + it "raises an error" do + expect { subject }.to raise_error(raised_error, expected_error_message) + end + end + + context "when validating with one ReqX restriction" do + subject do + constraint.validate(resource_restrictions: [reqx_restrictions.first, additional_restriction]) + end + + it "does not raise an error" do + expect { subject }.to_not raise_error + end + end + + context "when validating with many ReqX restrictions" do + let(:resource_restrictions) { reqx_restrictions[1, 2] } + let(:expected_error_message) { /#{Regexp.escape(resource_restrictions.to_s)}/ } + + subject do + constraint.validate(resource_restrictions: resource_restrictions + [additional_restriction]) + end + + it "raises an error" do + expect { subject }.to raise_error(raised_error, expected_error_message) + end + end + + context "when validating with all ReqX restrictions" do + let(:expected_error_message) { /#{Regexp.escape(reqx_restrictions.to_s)}/ } + + subject do + constraint.validate(resource_restrictions: reqx_restrictions + [additional_restriction]) + end + + it "raises an error" do + expect { subject }.to raise_error(raised_error, expected_error_message) + end + end + + end +end From dc0ad24cb06dc060eb45dc392a28b31139e3dc0c Mon Sep 17 00:00:00 2001 From: Kumbirai Tanekha Date: Wed, 27 Jul 2022 17:09:15 +0100 Subject: [PATCH 06/14] Refactor namespace labels retrieval --- .../authn_k8s/k8s_object_lookup.rb | 6 ---- .../authn_k8s/k8s_resource_validator.rb | 32 ++++++++++++------- app/domain/errors.rb | 7 +++- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb index 48b4641f55..84a65f9b1b 100644 --- a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb +++ b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb @@ -117,12 +117,6 @@ def pods_by_label(label_selector, namespace) k8s_client_for_method("get_pods").get_pods(label_selector: label_selector, namespace: namespace) end - # Locates namespace matching label selector in a namespace. - # - def namespace_by_label(namespace_name, label_selector) - k8s_client_for_method("get_namespaces").get_namespaces(field_selector: "metadata.name=#{namespace_name}", label_selector: label_selector) - end - # Look up an object according to the resource name. In Kubernetes, the # "resource" means something like ReplicaSet, Job, Deployment, etc. # diff --git a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb index 1de4109d09..042163a564 100644 --- a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb +++ b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb @@ -25,29 +25,37 @@ def valid_resource?(type:, name:) end def valid_namespace?(label_selector:) - # APPROACH - # - # namespaces = @k8s_object_lookup.namespace_by_label(namespace, label_selector) - # condition = namespaces.any? - # APPROACH + if (!label_selector.include?("=")) { + raise Errors::Authentication::AuthnK8s::InvalidNamespaceLabelSelector.new(label_selector) + } + namespace_object = @k8s_object_lookup.namespace_by_name(namespace) + labels_hash = namespace_object.metadata.labels.to_h + + validate_namespace_labels(label_selector, labels_hash) + end + + private - # in the spirit of https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go - labels_h = namespace_object.metadata.labels.to_h - label_selector_h = label_selector + def validate_labels(label_selector, labels_hash) + # In the spirit of https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go + label_selector_hash = label_selector .split(",") - .map{ |kv_pair| kv_pair = kv_pair.split("="); kv_pair[0] = kv_pair[0].to_sym; kv_pair } + .map{ |kv_pair| + kv_pair = kv_pair.split(/={1,2}/, 2) + kv_pair[0] = kv_pair[0].to_sym + kv_pair + } .to_h - condition = label_selector_h.all? { |k, v| labels_h[k] == v } + + condition = label_selector_h.all? { |k, v| labels_hash[k] == v } unless condition raise Errors::Authentication::AuthnK8s::NamespaceLabelSelectorMismatch.new(namespace, label_selector) end end - private - def retrieve_k8s_resource(type, name) @k8s_object_lookup.find_object_by_name( type, diff --git a/app/domain/errors.rb b/app/domain/errors.rb index 7e74ed7626..cf6ffd4702 100644 --- a/app/domain/errors.rb +++ b/app/domain/errors.rb @@ -291,7 +291,12 @@ module AuthnK8s NamespaceLabelSelectorMismatch = ::Util::TrackableErrorClass.new( msg: "Kubernetes namespace '{0-namespace}' does not match label-selector: {1-label-selector}", - code: "CONJ00026E" + code: "CONJ00083E" + ) + + InvalidNamespaceLabelSelector = ::Util::TrackableErrorClass.new( + msg: "Invalid namespace label selector {0-label-selector}: must adhere to format '='", + code: "CONJ00094E" ) ContainerNotFound = ::Util::TrackableErrorClass.new( From fb441de3893445d1a7268053a14cb167713e6423 Mon Sep 17 00:00:00 2001 From: Kumbirai Tanekha Date: Thu, 28 Jul 2022 16:52:08 +0100 Subject: [PATCH 07/14] Refactor and test namespace label validation --- .../authn_k8s/k8s_object_lookup.rb | 8 +- .../authn_k8s/k8s_resource_validator.rb | 39 +++++---- app/domain/errors.rb | 8 +- .../authn_k8s/k8s_resource_validator_spec.rb | 84 +++++++++++++++++++ 4 files changed, 115 insertions(+), 24 deletions(-) create mode 100644 spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb diff --git a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb index 84a65f9b1b..5c9deed792 100644 --- a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb +++ b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb @@ -104,11 +104,13 @@ def pod_by_name(podname, namespace) k8s_client_for_method("get_pod").get_pod(podname, namespace) end - # Locates the Namespace with a given name. + # Returns the labels hash for a Namespace with a given name. # # @return nil if no such Namespace exists. - def namespace_by_name(namespace) - k8s_client_for_method("get_namespace").get_namespace(namespace) + def namespace_labels_hash(namespace) + namespace_object = k8s_client_for_method("get_namespace").get_namespace(namespace) + + return namespace_object.metadata.labels.to_h unless namespace_object.nil? end # Locates pods matching label selector in a namespace. diff --git a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb index 042163a564..d1fd78f63d 100644 --- a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb +++ b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb @@ -25,37 +25,42 @@ def valid_resource?(type:, name:) end def valid_namespace?(label_selector:) - - if (!label_selector.include?("=")) { - raise Errors::Authentication::AuthnK8s::InvalidNamespaceLabelSelector.new(label_selector) - } - - namespace_object = @k8s_object_lookup.namespace_by_name(namespace) - labels_hash = namespace_object.metadata.labels.to_h - - validate_namespace_labels(label_selector, labels_hash) - end - - private - - def validate_labels(label_selector, labels_hash) + # Validates label selector and creates a hash # In the spirit of https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go + if label_selector.length == 0 + raise Errors::Authentication::AuthnK8s::InvalidLabelSelector.new(label_selector) + end label_selector_hash = label_selector .split(",") .map{ |kv_pair| kv_pair = kv_pair.split(/={1,2}/, 2) + + invalid ||= kv_pair.length != 2 + invalid ||= kv_pair[0].include?("!") + + if (invalid) + raise Errors::Authentication::AuthnK8s::InvalidLabelSelector.new(label_selector) + end + kv_pair[0] = kv_pair[0].to_sym kv_pair } .to_h - condition = label_selector_h.all? { |k, v| labels_hash[k] == v } + # Fetch namespace labels + # TODO: refactor this to have a generic label fetching method in @k8s_object_lookup + labels_hash = @k8s_object_lookup.namespace_labels_hash(namespace) - unless condition - raise Errors::Authentication::AuthnK8s::NamespaceLabelSelectorMismatch.new(namespace, label_selector) + # Validates label selector hash against labels hash + unless label_selector_hash.all? { |k, v| labels_hash[k] == v } + raise Errors::Authentication::AuthnK8s::LabelSelectorMismatch.new('namespace', namespace, label_selector) end + + return true end + private + def retrieve_k8s_resource(type, name) @k8s_object_lookup.find_object_by_name( type, diff --git a/app/domain/errors.rb b/app/domain/errors.rb index cf6ffd4702..475567eff0 100644 --- a/app/domain/errors.rb +++ b/app/domain/errors.rb @@ -289,13 +289,13 @@ module AuthnK8s code: "CONJ00026E" ) - NamespaceLabelSelectorMismatch = ::Util::TrackableErrorClass.new( - msg: "Kubernetes namespace '{0-namespace}' does not match label-selector: {1-label-selector}", + LabelSelectorMismatch = ::Util::TrackableErrorClass.new( + msg: "Kubernetes {0-resource-type} '{1-resource-id}' does not match label-selector: '{2-label-selector}'", code: "CONJ00083E" ) - InvalidNamespaceLabelSelector = ::Util::TrackableErrorClass.new( - msg: "Invalid namespace label selector {0-label-selector}: must adhere to format '='", + InvalidLabelSelector = ::Util::TrackableErrorClass.new( + msg: "Invalid label-selector '{0-label-selector}': must adhere to format '=,=,...', supports '=' and '=='", code: "CONJ00094E" ) diff --git a/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb b/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb new file mode 100644 index 0000000000..d719b4928c --- /dev/null +++ b/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe(Authentication::AuthnK8s::K8sResourceValidator) do + subject { + described_class.new(k8s_object_lookup: k8s_object_lookup, pod: pod) + } + + let(:k8s_object_lookup) { double("k8s_object_lookup") } + let(:pod) { double("pod") } + + context "#valid_namespace?" do + it 'raises error on empty label selector' do + expect { subject.valid_namespace?(label_selector: "") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::InvalidLabelSelector + ) + ) + end + + it 'raises error on invalid label selector' do + # No key-value pair + expect { subject.valid_namespace?(label_selector: "key,") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::InvalidLabelSelector + ) + ) + + # Unsupported operator + expect { subject.valid_namespace?(label_selector: "key!=value") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::InvalidLabelSelector + ) + ) + end + + it 'returns true for labels matching label-selector' do + pod.stub_chain("metadata.namespace").and_return("namespace_name") + allow(k8s_object_lookup).to receive(:namespace_labels_hash) + .with("namespace_name") + .and_return({ :key1 => "value1", :key2 => "value2"}) + + # Single key, single equals format + expect( + subject.valid_namespace?(label_selector: "key1=value1") + ).to be true + # Single key, double equals format + expect( + subject.valid_namespace?(label_selector: "key2==value2") + ).to be true + # Multiple keys + expect( + subject.valid_namespace?(label_selector: "key1=value1,key2=value2") + ).to be true + end + + it 'throws an error for labels not matching label-selector' do + pod.stub_chain("metadata.namespace").and_return("namespace_name") + allow(k8s_object_lookup).to receive(:namespace_labels_hash) + .with("namespace_name") + .and_return({ :key1 => "value1", :key2 => "value2"}) + + # Value mismatch + expect { subject.valid_namespace?(label_selector: "key1=notvalue") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::LabelSelectorMismatch + ) + ) + # Key not found + expect { subject.valid_namespace?(label_selector: "notfoundkey=value") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::LabelSelectorMismatch + ) + ) + # One of multiple keys does not match + expect { subject.valid_namespace?(label_selector: "key1=value1,notfoundkey=value") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::LabelSelectorMismatch + ) + ) + end + end +end From 1a7d45b47fcaffe67a1f14f3f9286c28162b84ed Mon Sep 17 00:00:00 2001 From: Kumbirai Tanekha Date: Thu, 28 Jul 2022 22:44:45 +0100 Subject: [PATCH 08/14] Clean up branch Revert Jenkinsfile, clean up Gemfile, build-and-publish-internal-appliance.sh and authn-k8s test server --- Dockerfile.appliance | 11 - Gemfile | 3 - Gemfile.lock | 5 - Jenkinsfile | 596 +++++++++++- build-and-publish-internal-appliance.sh | 16 +- spec/controllers/authn_k8s_test_server.rb | 118 --- .../controllers/bad:api.v1.getnamespaces.json | 8 - spec/controllers/good:api.json | 12 - .../good:api.v1.getnamespaces.json | 89 -- spec/controllers/good:api.v1.getpod.json | 420 --------- spec/controllers/good:api.v1.json | 548 ----------- spec/controllers/good:apis.all.json | 41 - spec/controllers/good:apis.json | 868 ------------------ spec/controllers/unauthorized.json | 9 - 14 files changed, 607 insertions(+), 2137 deletions(-) delete mode 100644 Dockerfile.appliance delete mode 100644 spec/controllers/authn_k8s_test_server.rb delete mode 100644 spec/controllers/bad:api.v1.getnamespaces.json delete mode 100644 spec/controllers/good:api.json delete mode 100644 spec/controllers/good:api.v1.getnamespaces.json delete mode 100644 spec/controllers/good:api.v1.getpod.json delete mode 100644 spec/controllers/good:api.v1.json delete mode 100644 spec/controllers/good:apis.all.json delete mode 100644 spec/controllers/good:apis.json delete mode 100644 spec/controllers/unauthorized.json diff --git a/Dockerfile.appliance b/Dockerfile.appliance deleted file mode 100644 index 9b7e786f29..0000000000 --- a/Dockerfile.appliance +++ /dev/null @@ -1,11 +0,0 @@ -FROM registry.tld/conjur-appliance:5.0-stable - -# Copy new source files -COPY app/domain/authentication/authn_k8s/authentication_request.rb /opt/conjur/possum/app/domain/authentication/authn_k8s/authentication_request.rb -COPY app/domain/authentication/authn_k8s/consts.rb /opt/conjur/possum/app/domain/authentication/authn_k8s/consts.rb -COPY app/domain/authentication/authn_k8s/k8s_object_lookup.rb /opt/conjur/possum/app/domain/authentication/authn_k8s/k8s_object_lookup.rb -COPY app/domain/authentication/authn_k8s/k8s_resource_validator.rb /opt/conjur/possum/app/domain/authentication/authn_k8s/k8s_resource_validator.rb -COPY app/domain/authentication/constraints/required_exclusive_constraint.rb /opt/conjur/possum/app/domain/authentication/constraints/required_exclusive_constraint.rb -COPY app/domain/errors.rb /opt/conjur/possum/app/domain/errors.rb - -RUN chown -R conjur:conjur /opt/conjur/possum/app diff --git a/Gemfile b/Gemfile index 2e9dc14cb7..1f06d52394 100644 --- a/Gemfile +++ b/Gemfile @@ -86,7 +86,6 @@ group :development, :test do gem 'cucumber', '~> 7.1' gem 'database_cleaner', '~> 1.8' gem 'debase', '~> 0.2.5.beta2' - gem 'faye-websocket' gem 'json_spec', '~> 1.1' gem 'faye-websocket' gem 'net-ssh' @@ -113,8 +112,6 @@ group :development, :test do gem 'vcr' gem 'webmock' gem 'webrick' - gem 'faye-websocket' - gem 'thin' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index ee548f5816..9e670b1cea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,7 +168,6 @@ GEM cucumber-core (~> 10.1, >= 10.1.0) cucumber-cucumber-expressions (~> 14.0, >= 14.0.0) cucumber-messages (~> 17.1, >= 17.1.1) - daemons (1.4.1) database_cleaner (1.8.5) debase (0.2.5.beta2) debase-ruby_core_source (>= 0.10.12) @@ -444,10 +443,6 @@ GEM sys-uname (1.2.2) ffi (~> 1.1) table_print (1.5.7) - thin (1.8.1) - daemons (~> 1.0, >= 1.0.9) - eventmachine (~> 1.0, >= 1.0.4) - rack (>= 1, < 3) thor (1.2.1) tzinfo (2.0.4) concurrent-ruby (~> 1.0) diff --git a/Jenkinsfile b/Jenkinsfile index 3d36f3cbaa..bcd6ca194e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -123,11 +123,603 @@ pipeline { } stages { - stage('Build and publish internal appliance') { + // Aborts any builds triggered by another project that wouldn't include any changes + stage ("Skip build if triggering job didn't create a release") { + when { + expression { + MODE == "SKIP" + } + } + steps { + script { + currentBuild.result = 'ABORTED' + error("Aborting build because this build was triggered from upstream, but no release was built") + } + } + } + // Generates a VERSION file based on the current build number and latest version in CHANGELOG.md + stage('Validate Changelog and set version') { + steps { + updateVersion("CHANGELOG.md", "${BUILD_NUMBER}") + stash name: 'version_info', includes: 'VERSION' + } + } + + stage('Fetch tags') { + steps { + withCredentials( + [ + usernameColonPassword( + credentialsId: 'conjur-jenkins-api', variable: 'GITCREDS' + ) + ] + ) { + sh ''' + git fetch --tags "$( + git remote get-url origin | + sed -e "s|https://|https://$GITCREDS@|" + )" + # print them out to make sure, can remove when this is robust + git tag + ''' + } + } + } + + stage('Validate Changelog') { + when { + expression { params.RUN_ONLY == '' } + } + steps { + sh 'ci/parse-changelog' + } + } + + stage('Build and test Conjur') { + when { + // Run tests only when ANY of the following is true: + // 1. A non-markdown file has changed. + // 2. It's running on the master branch (which includes nightly builds). + // 3. It's a tag-triggered build. + anyOf { + // Note: You cannot use "when"'s changeset condition here because it's + // not powerful enough to express "_only_ md files have changed". + // Dropping down to a git script was the easiest alternative. + expression { + 0 == sh( + returnStatus: true, + // A non-markdown file has changed. + script: ''' + git diff origin/master --name-only | + grep -v "^.*\\.md$" > /dev/null + ''' + ) + } + + // Always run the full pipeline on the master branch (which includes + // nightly builds) + branch "master" + + // Always run the full pipeline on tags of the form v* + tag "v*" + } + } + + stages { + stage('Build Docker Image') { + steps { + sh './build.sh --jenkins' + } + } + + stage('Push images to internal registry') { + steps { + // Push images to the internal registry so that they can be used + // by tests, even if the tests run on a different executor. + sh './publish-images.sh --internal' + } + } + + stage('Scan Docker Image') { + when { + expression { params.RUN_ONLY == '' } + } + parallel { + stage("Scan Docker Image for fixable issues") { + steps { + scanAndReport("conjur:${tagWithSHA()}", "HIGH", false) + } + } + stage("Scan Docker image for total issues") { + steps { + scanAndReport("conjur:${tagWithSHA()}", "NONE", true) + } + } + stage("Scan UBI-based Docker Image for fixable issues") { + steps { + scanAndReport("conjur-ubi:${tagWithSHA()}", "HIGH", false) + } + } + stage("Scan UBI-based Docker image for total issues") { + steps { + scanAndReport("conjur-ubi:${tagWithSHA()}", "NONE", true) + } + } + } + } + + // TODO: Add comments explaining which env vars are set here. + stage('Prepare For CodeClimate Coverage Report Submission') { + when { + expression { params.RUN_ONLY == '' } + } + steps { + catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { + script { + ccCoverage.dockerPrep() + sh 'mkdir -p coverage' + env.CODE_CLIMATE_PREPARED = "true" + } + } + } + } + + // Run outside parallel block to reduce main Jenkins executor load. + stage('Nightly Only') { + when { + expression { params.NIGHTLY } + } + + environment { + CUCUMBER_FILTER_TAGS = "${params.CUCUMBER_FILTER_TAGS}" + } + + stages { + stage('EE FIPS agent tests') { + agent { label 'executor-v2-rhel-ee' } + + steps { + unstash 'version_info' + // Catch errors so remaining steps always run. + catchError { + runConjurTests(params.RUN_ONLY) + } + + stash( + name: 'testResultEE', + includes: ''' + cucumber/*/*.*, + container_logs/*/*, + spec/reports/*.xml, + spec/reports-audit/*.xml, + cucumber/*/features/reports/**/*.xml + ''' + ) + } + + post { + always { + dir('ee-test'){ + unstash 'testResultEE' + } + + archiveArtifacts( + artifacts: "ee-test/cucumber/*/*.*", + fingerprint: false, + allowEmptyArchive: true + ) + + archiveArtifacts( + artifacts: "ee-test/container_logs/*/*", + fingerprint: false, + allowEmptyArchive: true + ) + + publishHTML( + reportDir: 'ee-test/cucumber', + reportFiles: ''' + api/cucumber_results.html, + authenticators_config/cucumber_results.html, + authenticators_azure/cucumber_results.html, + authenticators_ldap/cucumber_results.html, + authenticators_oidc/cucumber_results.html, + authenticators_jwt/cucumber_results.html, + authenticators_status/cucumber_results.html + policy/cucumber_results.html, + rotators/cucumber_results.html + ''', + reportName: 'EE Integration reports', + reportTitles: '', + allowMissing: false, + alwaysLinkToLastBuild: true, + keepAll: true + ) + } + } + } + } + } + + stage('Run environment tests in parallel') { + parallel { + stage('Standard agent tests') { + environment { + CUCUMBER_FILTER_TAGS = "${params.CUCUMBER_FILTER_TAGS}" + } + + steps { + runConjurTests(params.RUN_ONLY) + } + } + + stage('Azure Authenticator') { + when { + expression { + testShouldRun(params.RUN_ONLY, "azure_authenticator") + } + } + + agent { label 'azure-linux' } + + environment { + // TODO: Move this into the authenticators_azure bash script. + AZURE_AUTHN_INSTANCE_IP = sh( + script: 'curl "http://checkip.amazonaws.com"', + returnStdout: true + ).trim() + // TODO: Move this into the authenticators_azure bash script. + SYSTEM_ASSIGNED_IDENTITY = sh( + script: 'ci/test_suites/authenticators_azure/' + + 'get_system_assigned_identity.sh', + returnStdout: true + ).trim() + } + + steps { + unstash 'version_info' + // Grant access to this Jenkins agent's IP to AWS security groups + // This is required for access to the internal docker registry + // from outside EC2. + grantIPAccess() + sh( + 'summon -f ci/test_suites/authenticators_azure/secrets.yml ' + + 'ci/test authenticators_azure' + ) + } + + post { + always { + stash( + name: 'testResultAzure', + allowEmpty: true, + includes: ''' + cucumber/*azure*/*.*, + container_logs/*azure*/*, + cucumber_results*.json + ''' + ) + // Remove this Agent's IP from IPManager's prefix list + // There are a limited number of entries, so it remove it + // rather than waiting for it to expire. + removeIPAccess() + } + } + } + /** + * GCP Authenticator -- Token Stashing -- Stage 1 of 3 + * + * In this stage, a GCE instance node is allocated, a script runs + * and retrieves all the tokens that will be used in authn-gcp + * tests. The token are stashed, and later un-stashed and used in + * the stage that runs the GCP Authenticator tests. This way we can + * have a light-weight GCE instance that has no dependency on + * conjurops or git identities and is not open for SSH. + */ + stage('GCP Authenticator preparation - Allocate GCE Instance') { + when { + expression { + testShouldRun(params.RUN_ONLY, "gcp_authenticator") + } + } + steps { + echo '-- Allocating Google Compute Engine' + + script { + dir('ci/test_suites/authenticators_gcp') { + stash( + name: 'get_gce_tokens_script', + includes: ''' + get_gce_tokens_to_files.sh, + get_tokens_to_files.sh, + tokens_config.json + ''' + ) + } + + node('executor-v2-gcp-small') { + echo '-- Google Compute Engine allocated' + echo '-- Get compute engine instance project name from ' + + 'Google metadata server.' + // TODO: Move this into get_gce_tokens_to_files.sh + env.GCP_PROJECT = sh( + script: 'curl -s -H "Metadata-Flavor: Google" ' + + '"http://metadata.google.internal/computeMetadata/v1/' + + 'project/project-id"', + returnStdout: true + ).trim() + unstash('get_gce_tokens_script') + sh('./get_gce_tokens_to_files.sh') + stash( + name: 'authnGceTokens', + includes: 'gce_token_*', + allowEmpty:false + ) + } + } + } + post { + failure { + script { + env.GCP_ENV_ERROR = "true" + } + } + success { + script { + env.GCE_TOKENS_FETCHED = "true" + } + echo '-- Finished fetching GCE tokens.' + } + } + } + + /** + * GCP Authenticator -- Allocate Function -- Stage 2 of 3 + * + * In this stage, Google SDK container executes a script to deploy a + * function, the function accepts audience in query string and + * returns a token with that audience. All the tokens required for + * testings are obtained and written to function directory, the post + * stage branch deletes the function. This stage depends on stage: + * 'GCP Authenticator preparation - Allocate GCE Instance' to set + * the GCP project env var. + */ + stage('GCP Authenticator preparation - Allocate Google Function') { + when { + expression { + testShouldRun(params.RUN_ONLY, "gcp_authenticator") + } + } + environment { + GCP_FETCH_TOKEN_FUNCTION = "fetch_token_${BUILD_NUMBER}" + IDENTITY_TOKEN_FILE = 'identity-token' + GCP_OWNER_SERVICE_KEY_FILE = "sa-key-file.json" + } + steps { + echo "Waiting for GCP project name (Set by stage: " + + "'GCP Authenticator preparation - Allocate GCE Instance')" + timeout(time: 10, unit: 'MINUTES') { + waitUntil { + script { + return ( + env.GCP_PROJECT != null || env.GCP_ENV_ERROR == "true" + ) + } + } + } + script { + if (env.GCP_ENV_ERROR == "true") { + error('GCP_ENV_ERROR cannot deploy function') + } + + dir('ci/test_suites/authenticators_gcp') { + sh('summon ./deploy_function_and_get_tokens.sh') + } + } + } + post { + success { + echo "-- Google Cloud test env is ready" + script { + env.GCP_FUNC_TOKENS_FETCHED = "true" + } + } + failure { + echo "-- GCP function deployment stage failed" + script { + env.GCP_ENV_ERROR = "true" + } + } + always { + script { + dir('ci/test_suites/authenticators_gcp') { + sh ''' + # Cleanup Google function + summon ./run_gcloud.sh cleanup_function.sh + ''' + } + } + } + } + } + /** + * GCP Authenticator -- Run Tests -- Stage 3 of 3 + * + * We have two preparation stages before running the GCP + * Authenticator tests stage. This stage waits for GCP preparation + * stages to complete, un-stashes the tokens created in stage: 'GCP + * Authenticator preparation - Allocate GCE Instance' and runs the + * gcp-authn tests. + */ + stage('GCP Authenticator - Run Tests') { + when { + expression { + testShouldRun(params.RUN_ONLY, "gcp_authenticator") + } + } + steps { + echo('Waiting for GCP Tokens provisioned by prep stages.') + + timeout(time: 10, unit: 'MINUTES') { + waitUntil { + script { + return ( + env.GCP_ENV_ERROR == "true" || + ( + env.GCP_FUNC_TOKENS_FETCHED == "true" && + env.GCE_TOKENS_FETCHED == "true" + ) + ) + } + } + } + script { + if (env.GCP_ENV_ERROR == "true") { + error( + 'GCP_ENV_ERROR: Check logs for errors in stages 1 and 2' + ) + } + } + script { + dir('ci/test_suites/authenticators_gcp/tokens') { + unstash 'authnGceTokens' + } + sh 'ci/test authenticators_gcp' + } + } + } + } + } + } + + post { + success { + script { + if (env.BRANCH_NAME == 'master') { + build( + job:'../cyberark--secrets-provider-for-k8s/main', + wait: false + ) + } + } + } + + always { + script { + + // Only unstash azure if it ran. + if (testShouldRun(params.RUN_ONLY, "azure_authenticator")) { + unstash 'testResultAzure' + } + + // Make files available for download. + archiveFiles('container_logs/*/*') + archiveFiles('coverage/.resultset*.json') + archiveFiles('coverage/coverage.json') + archiveFiles('coverage/codeclimate.json') + archiveFiles( + 'ci/test_suites/authenticators_k8s/output/simplecov-resultset-authnk8s-gke.json' + ) + archiveFiles('cucumber/*/*.*') + + publishHTML([ + reportName: 'Integration reports', + reportDir: 'cucumber', + reportFiles: ''' + api/cucumber_results.html, + authenticators_config/cucumber_results.html, + authenticators_azure/cucumber_results.html, + authenticators_ldap/cucumber_results.html, + authenticators_oidc/cucumber_results.html, + authenticators_jwt/cucumber_results.html, + authenticators_gcp/cucumber_results.html, + authenticators_status/cucumber_results.html, + authenticators_k8s/cucumber_results.html, + policy/cucumber_results.html, + rotators/cucumber_results.html + ''', + reportTitles: '', + allowMissing: false, + alwaysLinkToLastBuild: true, + keepAll: true + ]) + + publishHTML( + reportName: 'Coverage Report', + reportDir: 'coverage', + reportFiles: 'index.html', + reportTitles: '', + allowMissing: false, + alwaysLinkToLastBuild: true, + keepAll: true + ) + junit(''' + spec/reports/*.xml, + spec/reports-audit/*.xml, + cucumber/*/features/reports/**/*.xml, + ee-test/spec/reports/*.xml, + ee-test/spec/reports-audit/*.xml, + ee-test/cucumber/*/features/reports/**/*.xml + ''' + ) + + // Make cucumber reports available as html report in Jenkins UI. + cucumber( + fileIncludePattern: '**/cucumber_results.json', + sortingMethod: 'ALPHABETICAL' + ) + } + } + } + } // end stage: build and test conjur + + stage('Submit Coverage Report') { + when { + expression { + env.CODE_CLIMATE_PREPARED == "true" + } + } steps{ - sh './build-and-publish-internal-appliance.sh' + sh 'ci/submit-coverage' } } + + stage("Release Conjur images and packages") { + when { + expression { + MODE == "RELEASE" + } + } + steps { + release { billOfMaterialsDirectory, assetDirectory -> + // Publish docker images + sh './publish-images.sh --edge --dockerhub' + + // Create deb and rpm packages + sh 'echo "CONJUR_VERSION=5" >> debify.env' + sh './package.sh' + archiveArtifacts artifacts: '*.deb', fingerprint: true + archiveArtifacts artifacts: '*.rpm', fingerprint: true + sh "cp *.rpm ${assetDirectory}/." + sh "cp *.deb ${assetDirectory}/." + + // Publish deb and rpm packages + sh './publish.sh' + } + } + } + } + + post { + always { + // Explanation of arguments: + // cleanupAndNotify(buildStatus, slackChannel, additionalMessage, ticket) + cleanupAndNotify( + currentBuild.currentResult, + '#conjur-core', + "${(params.NIGHTLY ? 'nightly' : '')}", + true + ) + } } } diff --git a/build-and-publish-internal-appliance.sh b/build-and-publish-internal-appliance.sh index b70654099d..883d623234 100755 --- a/build-and-publish-internal-appliance.sh +++ b/build-and-publish-internal-appliance.sh @@ -4,7 +4,12 @@ set -euo pipefail IMAGE="registry.tld/conjur-appliance:eval-authn-k8s-label-selector" -echo "FROM registry.tld/conjur-appliance:5.0-stable +echo "Building on top of stable appliance image and pushing to ${IMAGE}" + +echo " + +# --- +FROM registry.tld/conjur-appliance:5.0-stable # Copy new source files $( @@ -21,7 +26,12 @@ puts files.map {|file| "COPY #{file} /opt/conjur/possum/#{file}"}.join("\n") ' ) -RUN chown -R conjur:conjur /opt/conjur/possum/app" > Dockerfile.appliance +RUN chown -R conjur:conjur /opt/conjur/possum/app + +# --- + +" | \ + tee /dev/stderr | \ + docker build -f - -t "${IMAGE}" . -docker build -f ./Dockerfile.appliance -t "${IMAGE}" . docker push "${IMAGE}" diff --git a/spec/controllers/authn_k8s_test_server.rb b/spec/controllers/authn_k8s_test_server.rb deleted file mode 100644 index 3cd3adbe9b..0000000000 --- a/spec/controllers/authn_k8s_test_server.rb +++ /dev/null @@ -1,118 +0,0 @@ -require 'rack' -require 'faye/websocket' -require 'pathname' - -Faye::WebSocket.load_adapter('thin') - -class AuthnK8sTestServer - attr_reader :copied_content - attr_reader :subpath - - def initialize(subpath) - @subpath = Pathname.new(subpath).to_s - end - - def self.run(...) - # TODO: find out how to get random ports - Rack::Handler::Thin.run self.new(...), :Port => 1234, :Host => "0.0.0.0" do |server| - puts "server running with port=#{server.port}, @subpath=#{server.app.subpath}" - - end - end - - def self.run_async(...) - Thread.new do - self.run(...) - end - end - - def ws_call(env) - ws = Faye::WebSocket.new(env) - r = Rack::Request.new(env) - - stdin = r.params["stdin"] == "true" - - ws.on :open do |event| - p [:open] - next if stdin - - ws.send("well then :)".bytes.unshift(1)) - ws.close(1000) - end - - ws.on :message do |event| - type, *message_bytes = event.data.bytes - message = message_bytes.pack('c*') - p [:message, type, message] - - path = "AAAA" - tmp_cert = "AAAA" - log_file = "AAAA" - content = "AAAA" - mode = "AAAA" - - escaped = Regexp.escape <<~BASH_SCRIPT - #!/bin/sh - set -e - - cleanup() { - rm -f "#{tmp_cert}" - } - trap cleanup EXIT - - set_file_content() { - cat > "#{tmp_cert}" < "#{log_file}" 2>&1 - BASH_SCRIPT - escaped = escaped.gsub("AAAA", "(.*)") - - - @copied_content = message.match(Regexp.new(escaped)).captures[2] - p [:cert, @copied_content] - ws.close(1000) - end - - - ws.on :close do |event| - p [:close, event.code, event.reason] - ws = nil - end - - # Return async Rack response - ws.rack_response - end - - def call(env) - req = Rack::Request.new(env) - puts "[authn-k8s test server] Handling request: #{req.request_method} path=#{req.fullpath}" - - return ws_call(env) if Faye::WebSocket.websocket?(env) - - if req.fullpath == "#{subpath}/api/v1" - [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:api.v1.json")] ] - elsif req.fullpath == "#{subpath}/api" - [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:api.json")] ] - elsif req.fullpath == "#{subpath}/apis" - [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:apis.json")] ] - elsif req.fullpath.start_with?("#{subpath}/apis/") - [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:apis.all.json")] ] - elsif req.fullpath == "#{subpath}/api/v1/namespaces/default/pods/bash-8449b79d7-c2fwd" - [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:api.v1.getpod.json")] ] - elsif req.fullpath == "#{subpath}/api/v1/namespaces?labelSelector=field.cattle.io%2FprojectId%3Dp-q7s7z&fieldSelector=metadata.name%3Ddefault" - [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/good:api.v1.getnamespaces.json")] ] - elsif req.fullpath.start_with?("#{subpath}/api/v1/namespaces?") - [ 200, {"Content-Type" => "application/json"}, [File.read("./spec/controllers/bad:api.v1.getnamespaces.json")] ] - elsif req.fullpath == "#{subpath}/unauthorized" - [ 401, {"Content-Type" => "application/json"}, [File.read("./unauthorized.json")] ] - else - [ 200, {'Content-Type' => "application/json"}, [''] ] - end - end -end diff --git a/spec/controllers/bad:api.v1.getnamespaces.json b/spec/controllers/bad:api.v1.getnamespaces.json deleted file mode 100644 index 16f405ca99..0000000000 --- a/spec/controllers/bad:api.v1.getnamespaces.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "kind": "NamespaceList", - "apiVersion": "v1", - "metadata": { - "resourceVersion": "2445300" - }, - "items": [] -} diff --git a/spec/controllers/good:api.json b/spec/controllers/good:api.json deleted file mode 100644 index 2282062b73..0000000000 --- a/spec/controllers/good:api.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "kind": "APIVersions", - "versions": [ - "v1" - ], - "serverAddressByClientCIDRs": [ - { - "clientCIDR": "0.0.0.0/0", - "serverAddress": "10.0.147.71:6443" - } - ] -} diff --git a/spec/controllers/good:api.v1.getnamespaces.json b/spec/controllers/good:api.v1.getnamespaces.json deleted file mode 100644 index 4be3a4a0e1..0000000000 --- a/spec/controllers/good:api.v1.getnamespaces.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "kind": "NamespaceList", - "apiVersion": "v1", - "metadata": { - "resourceVersion": "2445300" - }, - "items": [ - { - "metadata": { - "name": "default", - "uid": "da93666e-c98d-44eb-9b04-eb59f2ccc000", - "resourceVersion": "1254", - "creationTimestamp": "2022-05-12T13:54:02Z", - "labels": { - "field.cattle.io/projectId": "p-q7s7z", - "kubernetes.io/metadata.name": "default" - }, - "annotations": { - "cattle.io/status": "{\"Conditions\":[{\"Type\":\"ResourceQuotaInit\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:26Z\"},{\"Type\":\"InitialRolesPopulated\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:31Z\"}]}", - "field.cattle.io/projectId": "c-4hbzx:p-q7s7z", - "lifecycle.cattle.io/create.namespace-auth": "true" - }, - "finalizers": [ - "controller.cattle.io/namespace-auth" - ], - "managedFields": [ - { - "manager": "kube-apiserver", - "operation": "Update", - "apiVersion": "v1", - "time": "2022-05-12T13:54:02Z", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:labels": { - ".": {}, - "f:kubernetes.io/metadata.name": {} - } - } - } - }, - { - "manager": "rancher", - "operation": "Update", - "apiVersion": "v1", - "time": "2022-05-12T13:55:25Z", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:annotations": { - ".": {}, - "f:cattle.io/status": {}, - "f:field.cattle.io/projectId": {}, - "f:lifecycle.cattle.io/create.namespace-auth": {} - }, - "f:finalizers": { - ".": {}, - "v:\"controller.cattle.io/namespace-auth\"": {} - } - } - } - }, - { - "manager": "agent", - "operation": "Update", - "apiVersion": "v1", - "time": "2022-05-12T13:55:49Z", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:labels": { - "f:field.cattle.io/projectId": {} - } - } - } - } - ] - }, - "spec": { - "finalizers": [ - "kubernetes" - ] - }, - "status": { - "phase": "Active" - } - } - ] -} diff --git a/spec/controllers/good:api.v1.getpod.json b/spec/controllers/good:api.v1.getpod.json deleted file mode 100644 index 6ff969bfa8..0000000000 --- a/spec/controllers/good:api.v1.getpod.json +++ /dev/null @@ -1,420 +0,0 @@ -{ - "kind":"Pod", - "apiVersion":"v1", - "metadata":{ - "name":"bash-8449b79d7-c2fwd", - "generateName":"bash-8449b79d7-", - "namespace":"default", - "uid":"d68bded9-3b9b-419e-baaf-1d43a74454d3", - "resourceVersion":"128550906", - "creationTimestamp":"2022-02-14T16:32:56Z", - "labels":{ - "pod-template-hash":"8449b79d7", - "run":"bash" - }, - "annotations":{ - "k8s.v1.cni.cncf.io/network-status":"[{\n \"name\": \"openshift-sdn\",\n \"interface\": \"eth0\",\n \"ips\": [\n \"10.129.2.18\"\n ],\n \"default\": true,\n \"dns\": {}\n}]", - "k8s.v1.cni.cncf.io/networks-status":"[{\n \"name\": \"openshift-sdn\",\n \"interface\": \"eth0\",\n \"ips\": [\n \"10.129.2.18\"\n ],\n \"default\": true,\n \"dns\": {}\n}]" - }, - "ownerReferences":[ - { - "apiVersion":"apps/v1", - "kind":"ReplicaSet", - "name":"bash-8449b79d7", - "uid":"6185eb73-a1fb-43b2-b1a5-1ad15b24dbe4", - "controller":true, - "blockOwnerDeletion":true - } - ], - "managedFields":[ - { - "manager":"kube-controller-manager", - "operation":"Update", - "apiVersion":"v1", - "time":"2022-02-14T16:32:56Z", - "fieldsType":"FieldsV1", - "fieldsV1":{ - "f:metadata":{ - "f:generateName":{ - - }, - "f:labels":{ - ".":{ - - }, - "f:pod-template-hash":{ - - }, - "f:run":{ - - } - }, - "f:ownerReferences":{ - ".":{ - - }, - "k:{\"uid\":\"6185eb73-a1fb-43b2-b1a5-1ad15b24dbe4\"}":{ - ".":{ - - }, - "f:apiVersion":{ - - }, - "f:blockOwnerDeletion":{ - - }, - "f:controller":{ - - }, - "f:kind":{ - - }, - "f:name":{ - - }, - "f:uid":{ - - } - } - } - }, - "f:spec":{ - "f:containers":{ - "k:{\"name\":\"bash\"}":{ - ".":{ - - }, - "f:image":{ - - }, - "f:imagePullPolicy":{ - - }, - "f:name":{ - - }, - "f:resources":{ - - }, - "f:stdin":{ - - }, - "f:terminationMessagePath":{ - - }, - "f:terminationMessagePolicy":{ - - }, - "f:tty":{ - - } - } - }, - "f:dnsPolicy":{ - - }, - "f:enableServiceLinks":{ - - }, - "f:restartPolicy":{ - - }, - "f:schedulerName":{ - - }, - "f:securityContext":{ - - }, - "f:terminationGracePeriodSeconds":{ - - } - } - } - }, - { - "manager":"multus", - "operation":"Update", - "apiVersion":"v1", - "time":"2022-02-14T16:32:59Z", - "fieldsType":"FieldsV1", - "fieldsV1":{ - "f:metadata":{ - "f:annotations":{ - ".":{ - - }, - "f:k8s.v1.cni.cncf.io/network-status":{ - - }, - "f:k8s.v1.cni.cncf.io/networks-status":{ - - } - } - } - } - }, - { - "manager":"kubelet", - "operation":"Update", - "apiVersion":"v1", - "time":"2022-02-14T16:33:12Z", - "fieldsType":"FieldsV1", - "fieldsV1":{ - "f:status":{ - "f:conditions":{ - "k:{\"type\":\"ContainersReady\"}":{ - ".":{ - - }, - "f:lastProbeTime":{ - - }, - "f:lastTransitionTime":{ - - }, - "f:status":{ - - }, - "f:type":{ - - } - }, - "k:{\"type\":\"Initialized\"}":{ - ".":{ - - }, - "f:lastProbeTime":{ - - }, - "f:lastTransitionTime":{ - - }, - "f:status":{ - - }, - "f:type":{ - - } - }, - "k:{\"type\":\"Ready\"}":{ - ".":{ - - }, - "f:lastProbeTime":{ - - }, - "f:lastTransitionTime":{ - - }, - "f:status":{ - - }, - "f:type":{ - - } - } - }, - "f:containerStatuses":{ - - }, - "f:hostIP":{ - - }, - "f:phase":{ - - }, - "f:podIP":{ - - }, - "f:podIPs":{ - ".":{ - - }, - "k:{\"ip\":\"10.129.2.18\"}":{ - ".":{ - - }, - "f:ip":{ - - } - } - }, - "f:startTime":{ - - } - } - } - } - ] - }, - "spec":{ - "volumes":[ - { - "name":"kube-api-access-p4wqn", - "projected":{ - "sources":[ - { - "serviceAccountToken":{ - "expirationSeconds":3607, - "path":"token" - } - }, - { - "configMap":{ - "name":"kube-root-ca.crt", - "items":[ - { - "key":"ca.crt", - "path":"ca.crt" - } - ] - } - }, - { - "downwardAPI":{ - "items":[ - { - "path":"namespace", - "fieldRef":{ - "apiVersion":"v1", - "fieldPath":"metadata.namespace" - } - } - ] - } - }, - { - "configMap":{ - "name":"openshift-service-ca.crt", - "items":[ - { - "key":"service-ca.crt", - "path":"service-ca.crt" - } - ] - } - } - ], - "defaultMode":420 - } - } - ], - "containers":[ - { - "name":"bash", - "image":"debian", - "resources":{ - - }, - "volumeMounts":[ - { - "name":"kube-api-access-p4wqn", - "readOnly":true, - "mountPath":"/var/run/secrets/kubernetes.io/serviceaccount" - } - ], - "terminationMessagePath":"/dev/termination-log", - "terminationMessagePolicy":"File", - "imagePullPolicy":"Always", - "stdin":true, - "tty":true - } - ], - "restartPolicy":"Always", - "terminationGracePeriodSeconds":30, - "dnsPolicy":"ClusterFirst", - "serviceAccountName":"default", - "serviceAccount":"default", - "nodeName":"ip-10-0-136-67.ec2.internal", - "securityContext":{ - - }, - "imagePullSecrets":[ - { - "name":"default-dockercfg-nxkrg" - }, - { - "name":"dockerhub" - }, - { - "name":"dockerpullsecret" - } - ], - "schedulerName":"default-scheduler", - "tolerations":[ - { - "key":"node.kubernetes.io/not-ready", - "operator":"Exists", - "effect":"NoExecute", - "tolerationSeconds":300 - }, - { - "key":"node.kubernetes.io/unreachable", - "operator":"Exists", - "effect":"NoExecute", - "tolerationSeconds":300 - } - ], - "priority":0, - "enableServiceLinks":true, - "preemptionPolicy":"PreemptLowerPriority" - }, - "status":{ - "phase":"Running", - "conditions":[ - { - "type":"Initialized", - "status":"True", - "lastProbeTime":null, - "lastTransitionTime":"2022-02-14T16:32:56Z" - }, - { - "type":"Ready", - "status":"True", - "lastProbeTime":null, - "lastTransitionTime":"2022-02-14T16:33:12Z" - }, - { - "type":"ContainersReady", - "status":"True", - "lastProbeTime":null, - "lastTransitionTime":"2022-02-14T16:33:12Z" - }, - { - "type":"PodScheduled", - "status":"True", - "lastProbeTime":null, - "lastTransitionTime":"2022-02-14T16:32:56Z" - } - ], - "hostIP":"10.0.136.67", - "podIP":"10.129.2.18", - "podIPs":[ - { - "ip":"10.129.2.18" - } - ], - "startTime":"2022-02-14T16:32:56Z", - "containerStatuses":[ - { - "name":"bash", - "state":{ - "running":{ - "startedAt":"2022-02-14T16:33:11Z" - } - }, - "lastState":{ - - }, - "ready":true, - "restartCount":0, - "image":"docker.io/library/debian:latest", - "imageID":"docker.io/library/debian@sha256:7d8264bf731fec57d807d1918bec0a16550f52a9766f0034b40f55c5b7dc3712", - "containerID":"cri-o://d0a3ea365223486d0c62a9a8ce4657debc346f390e44361aa3dbf40cc2a3d539", - "started":true - } - ], - "qosClass":"BestEffort" - } - } - \ No newline at end of file diff --git a/spec/controllers/good:api.v1.json b/spec/controllers/good:api.v1.json deleted file mode 100644 index 13e4750938..0000000000 --- a/spec/controllers/good:api.v1.json +++ /dev/null @@ -1,548 +0,0 @@ -{ - "kind": "APIResourceList", - "groupVersion": "v1", - "resources": [ - { - "name": "bindings", - "singularName": "", - "namespaced": true, - "kind": "Binding", - "verbs": [ - "create" - ] - }, - { - "name": "componentstatuses", - "singularName": "", - "namespaced": false, - "kind": "ComponentStatus", - "verbs": [ - "get", - "list" - ], - "shortNames": [ - "cs" - ] - }, - { - "name": "configmaps", - "singularName": "", - "namespaced": true, - "kind": "ConfigMap", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "cm" - ], - "storageVersionHash": "qFsyl6wFWjQ=" - }, - { - "name": "endpoints", - "singularName": "", - "namespaced": true, - "kind": "Endpoints", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "ep" - ], - "storageVersionHash": "fWeeMqaN/OA=" - }, - { - "name": "events", - "singularName": "", - "namespaced": true, - "kind": "Event", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "ev" - ], - "storageVersionHash": "r2yiGXH7wu8=" - }, - { - "name": "limitranges", - "singularName": "", - "namespaced": true, - "kind": "LimitRange", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "limits" - ], - "storageVersionHash": "EBKMFVe6cwo=" - }, - { - "name": "namespaces", - "singularName": "", - "namespaced": false, - "kind": "Namespace", - "verbs": [ - "create", - "delete", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "ns" - ], - "storageVersionHash": "Q3oi5N2YM8M=" - }, - { - "name": "namespaces/finalize", - "singularName": "", - "namespaced": false, - "kind": "Namespace", - "verbs": [ - "update" - ] - }, - { - "name": "namespaces/status", - "singularName": "", - "namespaced": false, - "kind": "Namespace", - "verbs": [ - "get", - "patch", - "update" - ] - }, - { - "name": "nodes", - "singularName": "", - "namespaced": false, - "kind": "Node", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "no" - ], - "storageVersionHash": "XwShjMxG9Fs=" - }, - { - "name": "nodes/proxy", - "singularName": "", - "namespaced": false, - "kind": "NodeProxyOptions", - "verbs": [ - "create", - "delete", - "get", - "patch", - "update" - ] - }, - { - "name": "nodes/status", - "singularName": "", - "namespaced": false, - "kind": "Node", - "verbs": [ - "get", - "patch", - "update" - ] - }, - { - "name": "persistentvolumeclaims", - "singularName": "", - "namespaced": true, - "kind": "PersistentVolumeClaim", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "pvc" - ], - "storageVersionHash": "QWTyNDq0dC4=" - }, - { - "name": "persistentvolumeclaims/status", - "singularName": "", - "namespaced": true, - "kind": "PersistentVolumeClaim", - "verbs": [ - "get", - "patch", - "update" - ] - }, - { - "name": "persistentvolumes", - "singularName": "", - "namespaced": false, - "kind": "PersistentVolume", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "pv" - ], - "storageVersionHash": "HN/zwEC+JgM=" - }, - { - "name": "persistentvolumes/status", - "singularName": "", - "namespaced": false, - "kind": "PersistentVolume", - "verbs": [ - "get", - "patch", - "update" - ] - }, - { - "name": "pods", - "singularName": "", - "namespaced": true, - "kind": "Pod", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "po" - ], - "categories": [ - "all" - ], - "storageVersionHash": "xPOwRZ+Yhw8=" - }, - { - "name": "pods/attach", - "singularName": "", - "namespaced": true, - "kind": "PodAttachOptions", - "verbs": [ - "create", - "get" - ] - }, - { - "name": "pods/binding", - "singularName": "", - "namespaced": true, - "kind": "Binding", - "verbs": [ - "create" - ] - }, - { - "name": "pods/eviction", - "singularName": "", - "namespaced": true, - "group": "policy", - "version": "v1beta1", - "kind": "Eviction", - "verbs": [ - "create" - ] - }, - { - "name": "pods/exec", - "singularName": "", - "namespaced": true, - "kind": "PodExecOptions", - "verbs": [ - "create", - "get" - ] - }, - { - "name": "pods/log", - "singularName": "", - "namespaced": true, - "kind": "Pod", - "verbs": [ - "get" - ] - }, - { - "name": "pods/portforward", - "singularName": "", - "namespaced": true, - "kind": "PodPortForwardOptions", - "verbs": [ - "create", - "get" - ] - }, - { - "name": "pods/proxy", - "singularName": "", - "namespaced": true, - "kind": "PodProxyOptions", - "verbs": [ - "create", - "delete", - "get", - "patch", - "update" - ] - }, - { - "name": "pods/status", - "singularName": "", - "namespaced": true, - "kind": "Pod", - "verbs": [ - "get", - "patch", - "update" - ] - }, - { - "name": "podtemplates", - "singularName": "", - "namespaced": true, - "kind": "PodTemplate", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "storageVersionHash": "LIXB2x4IFpk=" - }, - { - "name": "replicationcontrollers", - "singularName": "", - "namespaced": true, - "kind": "ReplicationController", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "rc" - ], - "categories": [ - "all" - ], - "storageVersionHash": "Jond2If31h0=" - }, - { - "name": "replicationcontrollers/scale", - "singularName": "", - "namespaced": true, - "group": "autoscaling", - "version": "v1", - "kind": "Scale", - "verbs": [ - "get", - "patch", - "update" - ] - }, - { - "name": "replicationcontrollers/status", - "singularName": "", - "namespaced": true, - "kind": "ReplicationController", - "verbs": [ - "get", - "patch", - "update" - ] - }, - { - "name": "resourcequotas", - "singularName": "", - "namespaced": true, - "kind": "ResourceQuota", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "quota" - ], - "storageVersionHash": "8uhSgffRX6w=" - }, - { - "name": "resourcequotas/status", - "singularName": "", - "namespaced": true, - "kind": "ResourceQuota", - "verbs": [ - "get", - "patch", - "update" - ] - }, - { - "name": "secrets", - "singularName": "", - "namespaced": true, - "kind": "Secret", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "storageVersionHash": "S6u1pOWzb84=" - }, - { - "name": "serviceaccounts", - "singularName": "", - "namespaced": true, - "kind": "ServiceAccount", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "sa" - ], - "storageVersionHash": "pbx9ZvyFpBE=" - }, - { - "name": "serviceaccounts/token", - "singularName": "", - "namespaced": true, - "group": "authentication.k8s.io", - "version": "v1", - "kind": "TokenRequest", - "verbs": [ - "create" - ] - }, - { - "name": "services", - "singularName": "", - "namespaced": true, - "kind": "Service", - "verbs": [ - "create", - "delete", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "svc" - ], - "categories": [ - "all" - ], - "storageVersionHash": "0/CO1lhkEBI=" - }, - { - "name": "services/proxy", - "singularName": "", - "namespaced": true, - "kind": "ServiceProxyOptions", - "verbs": [ - "create", - "delete", - "get", - "patch", - "update" - ] - }, - { - "name": "services/status", - "singularName": "", - "namespaced": true, - "kind": "Service", - "verbs": [ - "get", - "patch", - "update" - ] - } - ] -} \ No newline at end of file diff --git a/spec/controllers/good:apis.all.json b/spec/controllers/good:apis.all.json deleted file mode 100644 index 67ae6e68de..0000000000 --- a/spec/controllers/good:apis.all.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "kind": "APIResourceList", - "apiVersion": "v1", - "groupVersion": "autoscaling/v2beta2", - "resources": [ - { - "name": "horizontalpodautoscalers", - "singularName": "", - "namespaced": true, - "kind": "HorizontalPodAutoscaler", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ], - "shortNames": [ - "hpa" - ], - "categories": [ - "all" - ], - "storageVersionHash": "oQlkt7f5j/A=" - }, - { - "name": "horizontalpodautoscalers/status", - "singularName": "", - "namespaced": true, - "kind": "HorizontalPodAutoscaler", - "verbs": [ - "get", - "patch", - "update" - ] - } - ] -} diff --git a/spec/controllers/good:apis.json b/spec/controllers/good:apis.json deleted file mode 100644 index 6500e9289a..0000000000 --- a/spec/controllers/good:apis.json +++ /dev/null @@ -1,868 +0,0 @@ -{ - "kind": "APIGroupList", - "apiVersion": "v1", - "groups": [ - { - "name": "apiregistration.k8s.io", - "versions": [ - { - "groupVersion": "apiregistration.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "apiregistration.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "apiregistration.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "apps", - "versions": [ - { - "groupVersion": "apps/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "apps/v1", - "version": "v1" - } - }, - { - "name": "events.k8s.io", - "versions": [ - { - "groupVersion": "events.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "events.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "events.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "authentication.k8s.io", - "versions": [ - { - "groupVersion": "authentication.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "authentication.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "authentication.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "authorization.k8s.io", - "versions": [ - { - "groupVersion": "authorization.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "authorization.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "authorization.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "autoscaling", - "versions": [ - { - "groupVersion": "autoscaling/v1", - "version": "v1" - }, - { - "groupVersion": "autoscaling/v2beta1", - "version": "v2beta1" - }, - { - "groupVersion": "autoscaling/v2beta2", - "version": "v2beta2" - } - ], - "preferredVersion": { - "groupVersion": "autoscaling/v1", - "version": "v1" - } - }, - { - "name": "batch", - "versions": [ - { - "groupVersion": "batch/v1", - "version": "v1" - }, - { - "groupVersion": "batch/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "batch/v1", - "version": "v1" - } - }, - { - "name": "certificates.k8s.io", - "versions": [ - { - "groupVersion": "certificates.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "certificates.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "certificates.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "networking.k8s.io", - "versions": [ - { - "groupVersion": "networking.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "networking.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "networking.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "extensions", - "versions": [ - { - "groupVersion": "extensions/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "extensions/v1beta1", - "version": "v1beta1" - } - }, - { - "name": "policy", - "versions": [ - { - "groupVersion": "policy/v1", - "version": "v1" - }, - { - "groupVersion": "policy/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "policy/v1", - "version": "v1" - } - }, - { - "name": "rbac.authorization.k8s.io", - "versions": [ - { - "groupVersion": "rbac.authorization.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "rbac.authorization.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "rbac.authorization.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "storage.k8s.io", - "versions": [ - { - "groupVersion": "storage.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "storage.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "storage.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "admissionregistration.k8s.io", - "versions": [ - { - "groupVersion": "admissionregistration.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "admissionregistration.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "admissionregistration.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "apiextensions.k8s.io", - "versions": [ - { - "groupVersion": "apiextensions.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "apiextensions.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "apiextensions.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "scheduling.k8s.io", - "versions": [ - { - "groupVersion": "scheduling.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "scheduling.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "scheduling.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "coordination.k8s.io", - "versions": [ - { - "groupVersion": "coordination.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "coordination.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "coordination.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "node.k8s.io", - "versions": [ - { - "groupVersion": "node.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "node.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "node.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "discovery.k8s.io", - "versions": [ - { - "groupVersion": "discovery.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "discovery.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "discovery.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "flowcontrol.apiserver.k8s.io", - "versions": [ - { - "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta1", - "version": "v1beta1" - } - }, - { - "name": "apps.openshift.io", - "versions": [ - { - "groupVersion": "apps.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "apps.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "authorization.openshift.io", - "versions": [ - { - "groupVersion": "authorization.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "authorization.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "build.openshift.io", - "versions": [ - { - "groupVersion": "build.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "build.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "image.openshift.io", - "versions": [ - { - "groupVersion": "image.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "image.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "oauth.openshift.io", - "versions": [ - { - "groupVersion": "oauth.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "oauth.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "project.openshift.io", - "versions": [ - { - "groupVersion": "project.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "project.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "quota.openshift.io", - "versions": [ - { - "groupVersion": "quota.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "quota.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "route.openshift.io", - "versions": [ - { - "groupVersion": "route.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "route.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "security.openshift.io", - "versions": [ - { - "groupVersion": "security.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "security.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "template.openshift.io", - "versions": [ - { - "groupVersion": "template.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "template.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "user.openshift.io", - "versions": [ - { - "groupVersion": "user.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "user.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "packages.operators.coreos.com", - "versions": [ - { - "groupVersion": "packages.operators.coreos.com/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "packages.operators.coreos.com/v1", - "version": "v1" - } - }, - { - "name": "config.openshift.io", - "versions": [ - { - "groupVersion": "config.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "config.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "operator.openshift.io", - "versions": [ - { - "groupVersion": "operator.openshift.io/v1", - "version": "v1" - }, - { - "groupVersion": "operator.openshift.io/v1alpha1", - "version": "v1alpha1" - } - ], - "preferredVersion": { - "groupVersion": "operator.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "apiserver.openshift.io", - "versions": [ - { - "groupVersion": "apiserver.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "apiserver.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "autoscaling.openshift.io", - "versions": [ - { - "groupVersion": "autoscaling.openshift.io/v1", - "version": "v1" - }, - { - "groupVersion": "autoscaling.openshift.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "autoscaling.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "cloudcredential.openshift.io", - "versions": [ - { - "groupVersion": "cloudcredential.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "cloudcredential.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "conjur.cyberark.com", - "versions": [ - { - "groupVersion": "conjur.cyberark.com/v1", - "version": "v1" - }, - { - "groupVersion": "conjur.cyberark.com/v1alpha1", - "version": "v1alpha1" - } - ], - "preferredVersion": { - "groupVersion": "conjur.cyberark.com/v1", - "version": "v1" - } - }, - { - "name": "console.openshift.io", - "versions": [ - { - "groupVersion": "console.openshift.io/v1", - "version": "v1" - }, - { - "groupVersion": "console.openshift.io/v1alpha1", - "version": "v1alpha1" - } - ], - "preferredVersion": { - "groupVersion": "console.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "imageregistry.operator.openshift.io", - "versions": [ - { - "groupVersion": "imageregistry.operator.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "imageregistry.operator.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "ingress.operator.openshift.io", - "versions": [ - { - "groupVersion": "ingress.operator.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "ingress.operator.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "k8s.cni.cncf.io", - "versions": [ - { - "groupVersion": "k8s.cni.cncf.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "k8s.cni.cncf.io/v1", - "version": "v1" - } - }, - { - "name": "machineconfiguration.openshift.io", - "versions": [ - { - "groupVersion": "machineconfiguration.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "machineconfiguration.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "monitoring.coreos.com", - "versions": [ - { - "groupVersion": "monitoring.coreos.com/v1", - "version": "v1" - }, - { - "groupVersion": "monitoring.coreos.com/v1alpha1", - "version": "v1alpha1" - } - ], - "preferredVersion": { - "groupVersion": "monitoring.coreos.com/v1", - "version": "v1" - } - }, - { - "name": "network.openshift.io", - "versions": [ - { - "groupVersion": "network.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "network.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "network.operator.openshift.io", - "versions": [ - { - "groupVersion": "network.operator.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "network.operator.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "operators.coreos.com", - "versions": [ - { - "groupVersion": "operators.coreos.com/v2", - "version": "v2" - }, - { - "groupVersion": "operators.coreos.com/v1", - "version": "v1" - }, - { - "groupVersion": "operators.coreos.com/v1alpha2", - "version": "v1alpha2" - }, - { - "groupVersion": "operators.coreos.com/v1alpha1", - "version": "v1alpha1" - } - ], - "preferredVersion": { - "groupVersion": "operators.coreos.com/v2", - "version": "v2" - } - }, - { - "name": "samples.operator.openshift.io", - "versions": [ - { - "groupVersion": "samples.operator.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "samples.operator.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "security.internal.openshift.io", - "versions": [ - { - "groupVersion": "security.internal.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "security.internal.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "snapshot.storage.k8s.io", - "versions": [ - { - "groupVersion": "snapshot.storage.k8s.io/v1", - "version": "v1" - }, - { - "groupVersion": "snapshot.storage.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "snapshot.storage.k8s.io/v1", - "version": "v1" - } - }, - { - "name": "tuned.openshift.io", - "versions": [ - { - "groupVersion": "tuned.openshift.io/v1", - "version": "v1" - } - ], - "preferredVersion": { - "groupVersion": "tuned.openshift.io/v1", - "version": "v1" - } - }, - { - "name": "controlplane.operator.openshift.io", - "versions": [ - { - "groupVersion": "controlplane.operator.openshift.io/v1alpha1", - "version": "v1alpha1" - } - ], - "preferredVersion": { - "groupVersion": "controlplane.operator.openshift.io/v1alpha1", - "version": "v1alpha1" - } - }, - { - "name": "metal3.io", - "versions": [ - { - "groupVersion": "metal3.io/v1alpha1", - "version": "v1alpha1" - } - ], - "preferredVersion": { - "groupVersion": "metal3.io/v1alpha1", - "version": "v1alpha1" - } - }, - { - "name": "migration.k8s.io", - "versions": [ - { - "groupVersion": "migration.k8s.io/v1alpha1", - "version": "v1alpha1" - } - ], - "preferredVersion": { - "groupVersion": "migration.k8s.io/v1alpha1", - "version": "v1alpha1" - } - }, - { - "name": "whereabouts.cni.cncf.io", - "versions": [ - { - "groupVersion": "whereabouts.cni.cncf.io/v1alpha1", - "version": "v1alpha1" - } - ], - "preferredVersion": { - "groupVersion": "whereabouts.cni.cncf.io/v1alpha1", - "version": "v1alpha1" - } - }, - { - "name": "helm.openshift.io", - "versions": [ - { - "groupVersion": "helm.openshift.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "helm.openshift.io/v1beta1", - "version": "v1beta1" - } - }, - { - "name": "machine.openshift.io", - "versions": [ - { - "groupVersion": "machine.openshift.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "machine.openshift.io/v1beta1", - "version": "v1beta1" - } - }, - { - "name": "metrics.k8s.io", - "versions": [ - { - "groupVersion": "metrics.k8s.io/v1beta1", - "version": "v1beta1" - } - ], - "preferredVersion": { - "groupVersion": "metrics.k8s.io/v1beta1", - "version": "v1beta1" - } - } - ] -} diff --git a/spec/controllers/unauthorized.json b/spec/controllers/unauthorized.json deleted file mode 100644 index 06f1caeeaa..0000000000 --- a/spec/controllers/unauthorized.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "kind":"Status", - "apiVersion":"v1", - "metadata":{}, - "status":"Failure", - "message":"Unauthorized", - "reason":"Unauthorized", - "code":401 - } From 10838046fb90e11979166002230e170a2b5c7e92 Mon Sep 17 00:00:00 2001 From: John ODonnell Date: Fri, 29 Jul 2022 14:20:47 -0400 Subject: [PATCH 09/14] Refactor and test namespace label authentication --- .../authn_k8s/authentication_request.rb | 2 +- app/domain/authentication/authn_k8s/consts.rb | 4 +- .../authn_k8s/k8s_object_lookup.rb | 12 --- .../authn_k8s/k8s_resource_validator.rb | 7 +- app/domain/logs.rb | 14 +++- .../authn_k8s/k8s_resource_validator_spec.rb | 28 ++++++- .../authenticate_controller_authn_k8s_spec.rb | 4 +- .../authn_k8s/authn_k8s_test_server.rb | 6 +- .../authn_k8s/bad:api.v1.getnamespace.json | 13 +++ .../authn_k8s/bad:api.v1.getnamespaces.json | 8 -- .../authn_k8s/good:api.v1.getnamespace.json | 83 +++++++++++++++++++ 11 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 spec/support/authn_k8s/bad:api.v1.getnamespace.json delete mode 100644 spec/support/authn_k8s/bad:api.v1.getnamespaces.json create mode 100644 spec/support/authn_k8s/good:api.v1.getnamespace.json diff --git a/app/domain/authentication/authn_k8s/authentication_request.rb b/app/domain/authentication/authn_k8s/authentication_request.rb index 63a3f343d9..e26e5f26d9 100644 --- a/app/domain/authentication/authn_k8s/authentication_request.rb +++ b/app/domain/authentication/authn_k8s/authentication_request.rb @@ -18,7 +18,7 @@ def valid_restriction?(restriction) if restriction.value != @namespace raise Errors::Authentication::AuthnK8s::NamespaceMismatch.new(@namespace, restriction.value) end - when Restrictions::NAMESPACE_LABEL_SELECTOR + when Restrictions::NAMESPACE_LABEL @k8s_resource_validator.valid_namespace?(label_selector: restriction.value) else # Restrictions defined using '-', but the k8s client expects type with '_' instead. diff --git a/app/domain/authentication/authn_k8s/consts.rb b/app/domain/authentication/authn_k8s/consts.rb index 0a8f1800b5..a834db6b62 100644 --- a/app/domain/authentication/authn_k8s/consts.rb +++ b/app/domain/authentication/authn_k8s/consts.rb @@ -8,7 +8,7 @@ module AuthnK8s module Restrictions NAMESPACE = "namespace" - NAMESPACE_LABEL_SELECTOR = "namespace-label-selector" + NAMESPACE_LABEL = "namespace-label" SERVICE_ACCOUNT = "service-account" POD = "pod" DEPLOYMENT = "deployment" @@ -18,7 +18,7 @@ module Restrictions # This is not exactly a restriction, because it only validates container existence and not requesting container name. AUTHENTICATION_CONTAINER_NAME = "authentication-container-name" - REQUIRED_EXCLUSIVE = [NAMESPACE, NAMESPACE_LABEL_SELECTOR].freeze + REQUIRED_EXCLUSIVE = [NAMESPACE, NAMESPACE_LABEL].freeze RESOURCE_TYPE_EXCLUSIVE = [DEPLOYMENT, DEPLOYMENT_CONFIG, STATEFUL_SET].freeze OPTIONAL = [SERVICE_ACCOUNT, POD, AUTHENTICATION_CONTAINER_NAME].freeze PERMITTED = REQUIRED_EXCLUSIVE + RESOURCE_TYPE_EXCLUSIVE + OPTIONAL diff --git a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb index 5c9deed792..178c315de1 100644 --- a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb +++ b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb @@ -224,15 +224,3 @@ def load_additional_certs(ssl_cert_directory) end end end - -# rails c -# -# webservice = ::Authentication::Webservice.new( -# account: "rancherDemoAccount", -# authenticator_name: "authn-k8s", -# service_id: "demo/rancher" -# ) - -# k8s_object_lookup = Authentication::AuthnK8s::K8sObjectLookup.new(webservice) -# namespace = k8s_object_lookup.kube_client.get_namespace("default") -# namespace.metadata.labels.to_h diff --git a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb index d1fd78f63d..31b5105924 100644 --- a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb +++ b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb @@ -24,9 +24,11 @@ def valid_resource?(type:, name:) @logger.debug(LogMessages::Authentication::AuthnK8s::ValidatedK8sResource.new(type, name)) end + # Validates label selector and creates a hash + # In the spirit of https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go def valid_namespace?(label_selector:) - # Validates label selector and creates a hash - # In the spirit of https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go + @logger.debug(LogMessages::Authentication::AuthnK8s::ValidatingK8sResourceLabel.new('namespace', namespace, label_selector)) + if label_selector.length == 0 raise Errors::Authentication::AuthnK8s::InvalidLabelSelector.new(label_selector) end @@ -56,6 +58,7 @@ def valid_namespace?(label_selector:) raise Errors::Authentication::AuthnK8s::LabelSelectorMismatch.new('namespace', namespace, label_selector) end + @logger.debug(LogMessages::Authentication::AuthnK8s::ValidatedK8sResourceLabel.new('namespace', namespace, label_selector)) return true end diff --git a/app/domain/logs.rb b/app/domain/logs.rb index 3f0337b443..3dc3e547a8 100644 --- a/app/domain/logs.rb +++ b/app/domain/logs.rb @@ -234,12 +234,22 @@ module AuthnK8s ) ValidatingK8sResource = ::Util::TrackableLogMessageClass.new( - msg: "Validating K8s resource. Type:'{0}', Name: {1}", + msg: "Validating K8s resource. Type:'{0}', Name:'{1}'", + code: "CONJ00050D" + ) + + ValidatingK8sResourceLabel = ::Util::TrackableLogMessageClass.new( + msg: "Validating K8s resource. Type:'{0}', Name:'{1}', Label:'{2}'", code: "CONJ00050D" ) ValidatedK8sResource = ::Util::TrackableLogMessageClass.new( - msg: "Validated K8s resource. Type:'{0}', Name: {1}", + msg: "Validated K8s resource. Type:'{0}', Name:'{1}'", + code: "CONJ00051D" + ) + + ValidatedK8sResourceLabel = ::Util::TrackableLogMessageClass.new( + msg: "Validated K8s resource. Type:'{0}', Name:'{1}', Label:'{2}'", code: "CONJ00051D" ) end diff --git a/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb b/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb index d719b4928c..c1549812f5 100644 --- a/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb +++ b/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb @@ -3,13 +3,26 @@ require 'spec_helper' RSpec.describe(Authentication::AuthnK8s::K8sResourceValidator) do + let(:log_output) { StringIO.new } + let(:logger) { + Logger.new( + log_output, + formatter: proc do | severity, time, progname, msg | + "#{severity},#{msg}\n" + end) + } + subject { - described_class.new(k8s_object_lookup: k8s_object_lookup, pod: pod) + described_class.new(k8s_object_lookup: k8s_object_lookup, pod: pod, logger: logger) } let(:k8s_object_lookup) { double("k8s_object_lookup") } let(:pod) { double("pod") } + before(:example) do + pod.stub_chain("metadata.namespace").and_return("namespace_name") + end + context "#valid_namespace?" do it 'raises error on empty label selector' do expect { subject.valid_namespace?(label_selector: "") }.to( @@ -36,7 +49,6 @@ end it 'returns true for labels matching label-selector' do - pod.stub_chain("metadata.namespace").and_return("namespace_name") allow(k8s_object_lookup).to receive(:namespace_labels_hash) .with("namespace_name") .and_return({ :key1 => "value1", :key2 => "value2"}) @@ -45,14 +57,26 @@ expect( subject.valid_namespace?(label_selector: "key1=value1") ).to be true + expect(log_output.string.split("\n")).to include( + "DEBUG,CONJ00050D Validating K8s resource. Type:'namespace', Name:'namespace_name', Label:'key1=value1'", + "DEBUG,CONJ00051D Validated K8s resource. Type:'namespace', Name:'namespace_name', Label:'key1=value1'" + ) # Single key, double equals format expect( subject.valid_namespace?(label_selector: "key2==value2") ).to be true + expect(log_output.string.split("\n")).to include( + "DEBUG,CONJ00050D Validating K8s resource. Type:'namespace', Name:'namespace_name', Label:'key2==value2'", + "DEBUG,CONJ00051D Validated K8s resource. Type:'namespace', Name:'namespace_name', Label:'key2==value2'" + ) # Multiple keys expect( subject.valid_namespace?(label_selector: "key1=value1,key2=value2") ).to be true + expect(log_output.string.split("\n")).to include( + "DEBUG,CONJ00050D Validating K8s resource. Type:'namespace', Name:'namespace_name', Label:'key1=value1,key2=value2'", + "DEBUG,CONJ00051D Validated K8s resource. Type:'namespace', Name:'namespace_name', Label:'key1=value1,key2=value2'" + ) end it 'throws an error for labels not matching label-selector' do diff --git a/spec/controllers/authenticate_controller_authn_k8s_spec.rb b/spec/controllers/authenticate_controller_authn_k8s_spec.rb index 5fd25e3b97..a6b7a1f69e 100644 --- a/spec/controllers/authenticate_controller_authn_k8s_spec.rb +++ b/spec/controllers/authenticate_controller_authn_k8s_spec.rb @@ -35,9 +35,9 @@ def define_authenticator(account:, service_id:, host_id:) - !host id: #{host_id} annotations: - authn-k8s/namespace: default - # authn-k8s/namespace-label-selector: "field.cattle.io/projectId=p-q7s7z" + authn-k8s/namespace-label: "field.cattle.io/projectId=p-q7s7z" authn-k8s/authentication-container-name: bash + # authn-k8s/namespace: default # authn-k8s/service-account: # authn-k8s/deployment: # authn-k8s/deployment-config: diff --git a/spec/support/authn_k8s/authn_k8s_test_server.rb b/spec/support/authn_k8s/authn_k8s_test_server.rb index a0aa26a35d..6fd191715d 100644 --- a/spec/support/authn_k8s/authn_k8s_test_server.rb +++ b/spec/support/authn_k8s/authn_k8s_test_server.rb @@ -141,10 +141,10 @@ def call(env) [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("good:api.v1.getpod.json")] ] elsif req.path.start_with?("#{subpath}/api/v1/namespaces/default/pods/") [ 404, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("bad:api.v1.getpod.json")] ] - elsif req.fullpath == "#{subpath}/api/v1/namespaces?labelSelector=field.cattle.io%2FprojectId%3Dp-q7s7z&fieldSelector=metadata.name%3Ddefault" - [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("good:api.v1.getnamespaces.json")] ] + elsif req.fullpath == "#{subpath}/api/v1/namespaces/default" + [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("good:api.v1.getnamespace.json")]] elsif req.path.start_with?("#{subpath}/api/v1/namespaces") - [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("bad:api.v1.getnamespaces.json")] ] + [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("bad:api.v1.getnamespace.json")] ] # NOTE: Kubenertes clients make requests to a whole set of endpoints at initialization time. The only way we could find to make the clients # happy was to have this else branch return 200 with an empty JSON object. Ideally, this branch should return something like a 404. # TODO: Find a better way to satisfy Kubernetes client initialization in relation to the above note. diff --git a/spec/support/authn_k8s/bad:api.v1.getnamespace.json b/spec/support/authn_k8s/bad:api.v1.getnamespace.json new file mode 100644 index 0000000000..9956ef0df7 --- /dev/null +++ b/spec/support/authn_k8s/bad:api.v1.getnamespace.json @@ -0,0 +1,13 @@ +{ + "kind": "Status", + "apiVersion": "v1", + "metadata": {}, + "status": "Failure", + "message": "namespace \"ok\" not found", + "reason": "NotFound", + "details": { + "name": "ok", + "kind": "namespace" + }, + "code": 404 +} diff --git a/spec/support/authn_k8s/bad:api.v1.getnamespaces.json b/spec/support/authn_k8s/bad:api.v1.getnamespaces.json deleted file mode 100644 index 16f405ca99..0000000000 --- a/spec/support/authn_k8s/bad:api.v1.getnamespaces.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "kind": "NamespaceList", - "apiVersion": "v1", - "metadata": { - "resourceVersion": "2445300" - }, - "items": [] -} diff --git a/spec/support/authn_k8s/good:api.v1.getnamespace.json b/spec/support/authn_k8s/good:api.v1.getnamespace.json new file mode 100644 index 0000000000..8cf0b04461 --- /dev/null +++ b/spec/support/authn_k8s/good:api.v1.getnamespace.json @@ -0,0 +1,83 @@ +{ + "kind": "Namespace", + "apiVersion": "v1", + "metadata": { + "resourceVersion": "2445300", + "name": "default", + "uid": "da93666e-c98d-44eb-9b04-eb59f2ccc000", + "resourceVersion": "1254", + "creationTimestamp": "2022-05-12T13:54:02Z", + "labels": { + "field.cattle.io/projectId": "p-q7s7z", + "kubernetes.io/metadata.name": "default" + }, + "annotations": { + "cattle.io/status": "{\"Conditions\":[{\"Type\":\"ResourceQuotaInit\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:26Z\"},{\"Type\":\"InitialRolesPopulated\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:31Z\"}]}", + "field.cattle.io/projectId": "c-4hbzx:p-q7s7z", + "lifecycle.cattle.io/create.namespace-auth": "true" + }, + "finalizers": [ + "controller.cattle.io/namespace-auth" + ], + "managedFields": [ + { + "manager": "kube-apiserver", + "operation": "Update", + "apiVersion": "v1", + "time": "2022-05-12T13:54:02Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + } + }, + { + "manager": "rancher", + "operation": "Update", + "apiVersion": "v1", + "time": "2022-05-12T13:55:25Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:cattle.io/status": {}, + "f:field.cattle.io/projectId": {}, + "f:lifecycle.cattle.io/create.namespace-auth": {} + }, + "f:finalizers": { + ".": {}, + "v:\"controller.cattle.io/namespace-auth\"": {} + } + } + } + }, + { + "manager": "agent", + "operation": "Update", + "apiVersion": "v1", + "time": "2022-05-12T13:55:49Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:field.cattle.io/projectId": {} + } + } + } + } + ] + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} From 5d2bed7b78fcf9a92f9e0180f3ef7a3992d1fb4f Mon Sep 17 00:00:00 2001 From: John ODonnell Date: Fri, 29 Jul 2022 15:38:46 -0400 Subject: [PATCH 10/14] Refactor authn-k8s tests for reusability between test cases --- .../authenticate_controller_authn_k8s_spec.rb | 128 ++++++++++++------ 1 file changed, 86 insertions(+), 42 deletions(-) diff --git a/spec/controllers/authenticate_controller_authn_k8s_spec.rb b/spec/controllers/authenticate_controller_authn_k8s_spec.rb index a6b7a1f69e..90f86c5307 100644 --- a/spec/controllers/authenticate_controller_authn_k8s_spec.rb +++ b/spec/controllers/authenticate_controller_authn_k8s_spec.rb @@ -24,6 +24,25 @@ def apply_root_policy(account, policy_content:, expect_success: false) end end +def define_and_grant_host(account:, host_id:, namespace_annotation:, service_id:) + host_policy = %Q( +# Define test app host +- !host + id: #{host_id} + annotations: + authn-k8s/authentication-container-name: bash + #{namespace_annotation} + +# Grant app host authentication privileges +- !permit + role: !host #{host_id} + privilege: [ read, authenticate ] + resource: !webservice #{service_id} + ) + + apply_root_policy(account, policy_content: host_policy, expect_success: true) +end + def define_authenticator(account:, service_id:, host_id:) # Create authenticator instance by applying policy authenticator_policy = %Q( @@ -31,18 +50,6 @@ def define_authenticator(account:, service_id:, host_id:) # In the spirit of # https://docs.cyberark.com/Product-Doc/OnlineHelp/AAM-DAP/Latest/en/Content/Integrations/k8s-ocp/k8s-app-identity.htm?tocpath=Integrations%7COpenShift%252FKubernetes%7CSet%20up%20applications%7C_____4 -# Define test app host -- !host - id: #{host_id} - annotations: - authn-k8s/namespace-label: "field.cattle.io/projectId=p-q7s7z" - authn-k8s/authentication-container-name: bash - # authn-k8s/namespace: default - # authn-k8s/service-account: - # authn-k8s/deployment: - # authn-k8s/deployment-config: - # authn-k8s/stateful-set: - # Enroll a Kubernetes authentication service - !policy id: #{service_id} @@ -63,12 +70,6 @@ def define_authenticator(account:, service_id:, host_id:) - !webservice annotations: description: Authenticator service for K8s cluster - - # Grant app host authentication privileges - - !permit - role: !host /#{host_id} - privilege: [ read, authenticate ] - resource: !webservice ) apply_root_policy(account, policy_content: authenticator_policy, expect_success: true) @@ -106,7 +107,6 @@ def fake_authn_k8s_login(account, service_id, host_id:) ) end - def authn_k8s_login(authenticator_id:, host_id:) # Fake login hostpkey = OpenSSL::PKey::RSA.new(2048) @@ -133,19 +133,21 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe end describe AuthenticateController, :type => :request do - let(:account) { "rspec" } - let(:authenticator_id) { "authn-k8s/meow" } - let(:test_app_host) { "h-#{random_hex}" } - # Allows API calls to be made as the admin user - let(:admin_request_env) do - { 'HTTP_AUTHORIZATION' => "Token token=\"#{Base64.strict_encode64(Slosilo["authn:rspec"].signed_token("admin").to_json)}\"" } - end - # Test server is defined in the appropritate "around" hook for the test example let(:test_server) { @test_server } + before(:all) do + # Start fresh + DatabaseCleaner.clean_with(:truncation) + + # Init Slosilo key + Slosilo["authn:rspec"] ||= Slosilo::Key.new + Role.create(role_id: 'rspec:user:admin') + end + describe "#authenticate" do context "k8s api access contains subpath" do + around(:each) do |example| WebMock.disable_net_connect!(allow: 'http://localhost:1234') AuthnK8sTestServer.run_async( @@ -157,9 +159,7 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe end end - it "client successfully authenticates" do - service_id = "conjur/#{authenticator_id}" - + before(:each) do # Setup authenticator define_authenticator( account: account, @@ -177,10 +177,63 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe ca_cert: "---", service_account_token: "bearer token" ) - - # Authenticate - # first, ensure authenticator is enabled. Unfortunately there's no nicer way to do this since configuration used is that which is evaluated at load time! + # ensure authenticator is enabled. Unfortunately there's no nicer way to do this since configuration used is that which is evaluated at load time! allow_any_instance_of(Authentication::Webservices).to receive(:include?).and_return(true) + end + + let(:account) { "rspec" } + let(:authenticator_id) { "authn-k8s/meow" } + let(:service_id) { "conjur/#{authenticator_id}" } + let(:test_app_host) { "h-#{random_hex}" } + # Allows API calls to be made as the admin user + let(:admin_request_env) do + { 'HTTP_AUTHORIZATION' => "Token token=\"#{Base64.strict_encode64(Slosilo["authn:rspec"].signed_token("admin").to_json)}\"" } + end + + let(:namespace_name_annot) { 'authn-k8s/namespace: default' } + let(:namespace_label_annot) { 'authn-k8s/namespace-label: "field.cattle.io/projectId=p-q7s7z"' } + + it "client successfully authenticates with namespace name restriction" do + define_and_grant_host( + account: account, + host_id: test_app_host, + namespace_annotation: namespace_name_annot, + service_id: service_id + ) + + # NOTE: option to do an in-memory fake login request + # signed_cert = fake_authn_k8s_login(account, service_id, host_id: test_app_host) + + # Login request, grab the signed certificate from the fake server + authn_k8s_login( + authenticator_id: authenticator_id, + host_id: test_app_host + ) + signed_cert = test_server.copied_content + + # Authenticate request + authn_k8s_authenticate( + authenticator_id: authenticator_id, + account: account, + host_id: test_app_host, + signed_cert_pem: signed_cert.to_s + ) + + # Assertions + expect(response).to be_ok + token = Slosilo::JWT.parse_json(response.body) + expect(token.claims['sub']).to eq("host/#{test_app_host}") + expect(token.signature).to be + expect(token.claims).to have_key('iat') + end + + it "client successfully authenticates with namespace label restriction" do + define_and_grant_host( + account: account, + host_id: test_app_host, + namespace_annotation: namespace_label_annot, + service_id: service_id + ) # NOTE: option to do an in-memory fake login request # signed_cert = fake_authn_k8s_login(account, service_id, host_id: test_app_host) @@ -214,15 +267,6 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe # 2. Conjur Authentication errors # ... end - - before(:all) do - # Start fresh - DatabaseCleaner.clean_with(:truncation) - - # Init Slosilo key - Slosilo["authn:rspec"] ||= Slosilo::Key.new - Role.create(role_id: 'rspec:user:admin') - end end # bundle exec rspec --format documentation ./spec/controllers/authenticate_controller_authn_k8s_spec.rb From 80af822281826d059e52282c8d0d7ea7dd04379e Mon Sep 17 00:00:00 2001 From: John ODonnell Date: Fri, 29 Jul 2022 17:56:39 -0400 Subject: [PATCH 11/14] Fix refactor - revert to namespace-label-selector - break out resource validator logs test case - remove unneeded namespaces list response JSON --- .../authn_k8s/authentication_request.rb | 2 +- app/domain/authentication/authn_k8s/consts.rb | 4 +- app/domain/logs.rb | 14 +-- .../authn_k8s/k8s_resource_validator_spec.rb | 58 ++++++------ .../authn_k8s/good:api.v1.getnamespaces.json | 89 ------------------- 5 files changed, 42 insertions(+), 125 deletions(-) delete mode 100644 spec/support/authn_k8s/good:api.v1.getnamespaces.json diff --git a/app/domain/authentication/authn_k8s/authentication_request.rb b/app/domain/authentication/authn_k8s/authentication_request.rb index e26e5f26d9..63a3f343d9 100644 --- a/app/domain/authentication/authn_k8s/authentication_request.rb +++ b/app/domain/authentication/authn_k8s/authentication_request.rb @@ -18,7 +18,7 @@ def valid_restriction?(restriction) if restriction.value != @namespace raise Errors::Authentication::AuthnK8s::NamespaceMismatch.new(@namespace, restriction.value) end - when Restrictions::NAMESPACE_LABEL + when Restrictions::NAMESPACE_LABEL_SELECTOR @k8s_resource_validator.valid_namespace?(label_selector: restriction.value) else # Restrictions defined using '-', but the k8s client expects type with '_' instead. diff --git a/app/domain/authentication/authn_k8s/consts.rb b/app/domain/authentication/authn_k8s/consts.rb index a834db6b62..0a8f1800b5 100644 --- a/app/domain/authentication/authn_k8s/consts.rb +++ b/app/domain/authentication/authn_k8s/consts.rb @@ -8,7 +8,7 @@ module AuthnK8s module Restrictions NAMESPACE = "namespace" - NAMESPACE_LABEL = "namespace-label" + NAMESPACE_LABEL_SELECTOR = "namespace-label-selector" SERVICE_ACCOUNT = "service-account" POD = "pod" DEPLOYMENT = "deployment" @@ -18,7 +18,7 @@ module Restrictions # This is not exactly a restriction, because it only validates container existence and not requesting container name. AUTHENTICATION_CONTAINER_NAME = "authentication-container-name" - REQUIRED_EXCLUSIVE = [NAMESPACE, NAMESPACE_LABEL].freeze + REQUIRED_EXCLUSIVE = [NAMESPACE, NAMESPACE_LABEL_SELECTOR].freeze RESOURCE_TYPE_EXCLUSIVE = [DEPLOYMENT, DEPLOYMENT_CONFIG, STATEFUL_SET].freeze OPTIONAL = [SERVICE_ACCOUNT, POD, AUTHENTICATION_CONTAINER_NAME].freeze PERMITTED = REQUIRED_EXCLUSIVE + RESOURCE_TYPE_EXCLUSIVE + OPTIONAL diff --git a/app/domain/logs.rb b/app/domain/logs.rb index 3dc3e547a8..d06e88c81d 100644 --- a/app/domain/logs.rb +++ b/app/domain/logs.rb @@ -238,19 +238,19 @@ module AuthnK8s code: "CONJ00050D" ) - ValidatingK8sResourceLabel = ::Util::TrackableLogMessageClass.new( - msg: "Validating K8s resource. Type:'{0}', Name:'{1}', Label:'{2}'", - code: "CONJ00050D" - ) - ValidatedK8sResource = ::Util::TrackableLogMessageClass.new( msg: "Validated K8s resource. Type:'{0}', Name:'{1}'", code: "CONJ00051D" ) + ValidatingK8sResourceLabel = ::Util::TrackableLogMessageClass.new( + msg: "Validating K8s resource using label selector. Type:'{0}', Name:'{1}', Label:'{2}'", + code: "CONJ00145D" + ) + ValidatedK8sResourceLabel = ::Util::TrackableLogMessageClass.new( - msg: "Validated K8s resource. Type:'{0}', Name:'{1}', Label:'{2}'", - code: "CONJ00051D" + msg: "Validated K8s resource using label selector. Type:'{0}', Name:'{1}', Label:'{2}'", + code: "CONJ00146D" ) end diff --git a/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb b/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb index c1549812f5..65c60e0503 100644 --- a/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb +++ b/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb @@ -16,12 +16,19 @@ described_class.new(k8s_object_lookup: k8s_object_lookup, pod: pod, logger: logger) } - let(:k8s_object_lookup) { double("k8s_object_lookup") } - let(:pod) { double("pod") } + let(:k8s_object_lookup) { + double("k8s_object_lookup").tap { |d| + allow(d).to receive(:namespace_labels_hash) + .with("namespace_name") + .and_return({ :key1 => "value1", :key2 => "value2" }) + } + } - before(:example) do - pod.stub_chain("metadata.namespace").and_return("namespace_name") - end + let(:pod) { + double("pod").tap { |d| + d.stub_chain("metadata.namespace").and_return("namespace_name") + } + } context "#valid_namespace?" do it 'raises error on empty label selector' do @@ -49,42 +56,21 @@ end it 'returns true for labels matching label-selector' do - allow(k8s_object_lookup).to receive(:namespace_labels_hash) - .with("namespace_name") - .and_return({ :key1 => "value1", :key2 => "value2"}) - # Single key, single equals format expect( subject.valid_namespace?(label_selector: "key1=value1") ).to be true - expect(log_output.string.split("\n")).to include( - "DEBUG,CONJ00050D Validating K8s resource. Type:'namespace', Name:'namespace_name', Label:'key1=value1'", - "DEBUG,CONJ00051D Validated K8s resource. Type:'namespace', Name:'namespace_name', Label:'key1=value1'" - ) # Single key, double equals format expect( subject.valid_namespace?(label_selector: "key2==value2") ).to be true - expect(log_output.string.split("\n")).to include( - "DEBUG,CONJ00050D Validating K8s resource. Type:'namespace', Name:'namespace_name', Label:'key2==value2'", - "DEBUG,CONJ00051D Validated K8s resource. Type:'namespace', Name:'namespace_name', Label:'key2==value2'" - ) # Multiple keys expect( subject.valid_namespace?(label_selector: "key1=value1,key2=value2") ).to be true - expect(log_output.string.split("\n")).to include( - "DEBUG,CONJ00050D Validating K8s resource. Type:'namespace', Name:'namespace_name', Label:'key1=value1,key2=value2'", - "DEBUG,CONJ00051D Validated K8s resource. Type:'namespace', Name:'namespace_name', Label:'key1=value1,key2=value2'" - ) end it 'throws an error for labels not matching label-selector' do - pod.stub_chain("metadata.namespace").and_return("namespace_name") - allow(k8s_object_lookup).to receive(:namespace_labels_hash) - .with("namespace_name") - .and_return({ :key1 => "value1", :key2 => "value2"}) - # Value mismatch expect { subject.valid_namespace?(label_selector: "key1=notvalue") }.to( raise_error( @@ -104,5 +90,25 @@ ) ) end + + it 'logs before label-selector validation begins, and after success' do + subject.valid_namespace?(label_selector: "key1=value1") + + expect(log_output.string.split("\n")).to include( + "DEBUG,CONJ00145D Validating K8s resource using label selector. Type:'namespace', Name:'namespace_name', Label:'key1=value1'", + "DEBUG,CONJ00146D Validated K8s resource using label selector. Type:'namespace', Name:'namespace_name', Label:'key1=value1'" + ) + end + + it 'logs before label-selector validation begins, but not after failure' do + expect { subject.valid_namespace?(label_selector: "key1=notvalue") }.to raise_error + + expect(log_output.string.split("\n")).to include( + "DEBUG,CONJ00145D Validating K8s resource using label selector. Type:'namespace', Name:'namespace_name', Label:'key1=notvalue'", + ) + expect(log_output.string.split("\n")).not_to include( + "DEBUG,CONJ00146D Validated K8s resource using label selector. Type:'namespace', Name:'namespace_name', Label:'key1=notvalue'" + ) + end end end diff --git a/spec/support/authn_k8s/good:api.v1.getnamespaces.json b/spec/support/authn_k8s/good:api.v1.getnamespaces.json deleted file mode 100644 index 4be3a4a0e1..0000000000 --- a/spec/support/authn_k8s/good:api.v1.getnamespaces.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "kind": "NamespaceList", - "apiVersion": "v1", - "metadata": { - "resourceVersion": "2445300" - }, - "items": [ - { - "metadata": { - "name": "default", - "uid": "da93666e-c98d-44eb-9b04-eb59f2ccc000", - "resourceVersion": "1254", - "creationTimestamp": "2022-05-12T13:54:02Z", - "labels": { - "field.cattle.io/projectId": "p-q7s7z", - "kubernetes.io/metadata.name": "default" - }, - "annotations": { - "cattle.io/status": "{\"Conditions\":[{\"Type\":\"ResourceQuotaInit\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:26Z\"},{\"Type\":\"InitialRolesPopulated\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:31Z\"}]}", - "field.cattle.io/projectId": "c-4hbzx:p-q7s7z", - "lifecycle.cattle.io/create.namespace-auth": "true" - }, - "finalizers": [ - "controller.cattle.io/namespace-auth" - ], - "managedFields": [ - { - "manager": "kube-apiserver", - "operation": "Update", - "apiVersion": "v1", - "time": "2022-05-12T13:54:02Z", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:labels": { - ".": {}, - "f:kubernetes.io/metadata.name": {} - } - } - } - }, - { - "manager": "rancher", - "operation": "Update", - "apiVersion": "v1", - "time": "2022-05-12T13:55:25Z", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:annotations": { - ".": {}, - "f:cattle.io/status": {}, - "f:field.cattle.io/projectId": {}, - "f:lifecycle.cattle.io/create.namespace-auth": {} - }, - "f:finalizers": { - ".": {}, - "v:\"controller.cattle.io/namespace-auth\"": {} - } - } - } - }, - { - "manager": "agent", - "operation": "Update", - "apiVersion": "v1", - "time": "2022-05-12T13:55:49Z", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:labels": { - "f:field.cattle.io/projectId": {} - } - } - } - } - ] - }, - "spec": { - "finalizers": [ - "kubernetes" - ] - }, - "status": { - "phase": "Active" - } - } - ] -} From 35b9a18dbdd7e09ac808b64d3928685892695b7a Mon Sep 17 00:00:00 2001 From: John ODonnell Date: Fri, 29 Jul 2022 18:51:33 -0400 Subject: [PATCH 12/14] Refactor errors, authn-k8s test server --- app/domain/errors.rb | 12 +-- .../authenticate_controller_authn_k8s_spec.rb | 77 ++++++++++++++----- .../authn_k8s/authn_k8s_test_server.rb | 2 +- 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/app/domain/errors.rb b/app/domain/errors.rb index 475567eff0..4df16ab04f 100644 --- a/app/domain/errors.rb +++ b/app/domain/errors.rb @@ -700,12 +700,6 @@ module Constraints code: "CONJ00069E" ) - IllegalRequiredExclusiveCombination = ::Util::TrackableErrorClass.new( - msg: "Role must have exactly one of the following required constraints: " \ - "{0-constraints}. Role configured with {1-provided}", - code: "CONJ00069E" - ) - NonPermittedRestrictionGiven = ::Util::TrackableErrorClass.new( msg: "Role can't have one of these none permitted restrictions '{0-restrictions}'", code: "CONJ00069E" @@ -715,6 +709,12 @@ module Constraints msg: "Role must have at least one relevant annotation", code: "CONJ00099E" ) + + IllegalRequiredExclusiveCombination = ::Util::TrackableErrorClass.new( + msg: "Role must have exactly one of the following required constraints: " \ + "{0-constraints}. Role configured with {1-provided}", + code: "CONJ00131E" + ) end end diff --git a/spec/controllers/authenticate_controller_authn_k8s_spec.rb b/spec/controllers/authenticate_controller_authn_k8s_spec.rb index 90f86c5307..aece8616ea 100644 --- a/spec/controllers/authenticate_controller_authn_k8s_spec.rb +++ b/spec/controllers/authenticate_controller_authn_k8s_spec.rb @@ -24,14 +24,13 @@ def apply_root_policy(account, policy_content:, expect_success: false) end end -def define_and_grant_host(account:, host_id:, namespace_annotation:, service_id:) +def define_and_grant_host(account:, host_id:, annotations:, service_id:) host_policy = %Q( # Define test app host - !host id: #{host_id} annotations: - authn-k8s/authentication-container-name: bash - #{namespace_annotation} +#{annotations.map{ |k,v| "#{k}: #{v}" }.join("\n").indent(4)} # Grant app host authentication privileges - !permit @@ -132,10 +131,30 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe post("/#{authenticator_id}/#{account}/#{escaped_host_id}/authenticate", env: payload) end +def capture_args(obj, method) + args = [] + original_method = obj.method(method) + allow(obj).to receive(method) { |arg| + args.push(arg) + original_method.call(arg) + } + args +end + describe AuthenticateController, :type => :request do # Test server is defined in the appropritate "around" hook for the test example let(:test_server) { @test_server } + let(:account) { "rspec" } + let(:authenticator_id) { "authn-k8s/meow" } + let(:service_id) { "conjur/#{authenticator_id}" } + let(:test_app_host) { "h-#{random_hex}" } + + # Allows API calls to be made as the admin user + let(:admin_request_env) do + { 'HTTP_AUTHORIZATION' => "Token token=\"#{Base64.strict_encode64(Slosilo["authn:rspec"].signed_token("admin").to_json)}\"" } + end + before(:all) do # Start fresh DatabaseCleaner.clean_with(:truncation) @@ -146,8 +165,7 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe end describe "#authenticate" do - context "k8s api access contains subpath" do - + context "k8s mock server" do around(:each) do |example| WebMock.disable_net_connect!(allow: 'http://localhost:1234') AuthnK8sTestServer.run_async( @@ -181,23 +199,14 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe allow_any_instance_of(Authentication::Webservices).to receive(:include?).and_return(true) end - let(:account) { "rspec" } - let(:authenticator_id) { "authn-k8s/meow" } - let(:service_id) { "conjur/#{authenticator_id}" } - let(:test_app_host) { "h-#{random_hex}" } - # Allows API calls to be made as the admin user - let(:admin_request_env) do - { 'HTTP_AUTHORIZATION' => "Token token=\"#{Base64.strict_encode64(Slosilo["authn:rspec"].signed_token("admin").to_json)}\"" } - end - - let(:namespace_name_annot) { 'authn-k8s/namespace: default' } - let(:namespace_label_annot) { 'authn-k8s/namespace-label: "field.cattle.io/projectId=p-q7s7z"' } - it "client successfully authenticates with namespace name restriction" do define_and_grant_host( account: account, host_id: test_app_host, - namespace_annotation: namespace_name_annot, + annotations: { + "authn-k8s/authentication-container-name" => "bash", + "authn-k8s/namespace" => "default" + }, service_id: service_id ) @@ -231,7 +240,10 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe define_and_grant_host( account: account, host_id: test_app_host, - namespace_annotation: namespace_label_annot, + annotations: { + "authn-k8s/authentication-container-name" => "bash", + "authn-k8s/namespace-label-selector" => "field.cattle.io/projectId=p-q7s7z" + }, service_id: service_id ) @@ -260,6 +272,33 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe expect(token.signature).to be expect(token.claims).to have_key('iat') end + + it "client fails when given both namespace name and label restriction" do + define_and_grant_host( + account: account, + host_id: test_app_host, + annotations: { + "authn-k8s/namespace" => "default", + "authn-k8s/namespace-label-selector" => "field.cattle.io/projectId=p-q7s7z" , + "authn-k8s/authentication-container-name" => "bash" + }, + service_id: service_id + ) + + info_log_args = capture_args(Rails.logger, :info) + + # Login request, grab the signed certificate from the fake server + authn_k8s_login( + authenticator_id: authenticator_id, + host_id: test_app_host + ) + + expect(info_log_args).to satisfy { |args| + args.any? { |arg| + arg.to_s.include?("CONJ00131E") + } + } + end end # TODO: Add more scenarios diff --git a/spec/support/authn_k8s/authn_k8s_test_server.rb b/spec/support/authn_k8s/authn_k8s_test_server.rb index 6fd191715d..d00f1638d5 100644 --- a/spec/support/authn_k8s/authn_k8s_test_server.rb +++ b/spec/support/authn_k8s/authn_k8s_test_server.rb @@ -144,7 +144,7 @@ def call(env) elsif req.fullpath == "#{subpath}/api/v1/namespaces/default" [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("good:api.v1.getnamespace.json")]] elsif req.path.start_with?("#{subpath}/api/v1/namespaces") - [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("bad:api.v1.getnamespace.json")] ] + [ 404, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("bad:api.v1.getnamespace.json")] ] # NOTE: Kubenertes clients make requests to a whole set of endpoints at initialization time. The only way we could find to make the clients # happy was to have this else branch return 200 with an empty JSON object. Ideally, this branch should return something like a 404. # TODO: Find a better way to satisfy Kubernetes client initialization in relation to the above note. From b6e4ff79d34b2286915964b6e2eff4fee2c8dd5d Mon Sep 17 00:00:00 2001 From: John ODonnell Date: Fri, 29 Jul 2022 19:11:38 -0400 Subject: [PATCH 13/14] Update Changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 575c7eda49..d5b47d216d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Nothing should go in this section, please add to the latest unreleased version (and update the corresponding date), or add a new version. +## [1.18.1] - 2022-07-29 + +### Added +- Adds support for namespace label based identity scope for the Kubernetes Authenticator + [cyberark/conjur#2613](https://github.com/cyberark/conjur/pull/2613) + ## [1.18.0] - 2022-07-12 ### Changed - Adds support for authentication using OIDC's code authorization flow From e7d17272db3fb56250a9ce6f391d127a07f52634 Mon Sep 17 00:00:00 2001 From: Kumbirai Tanekha Date: Mon, 1 Aug 2022 08:59:38 +0100 Subject: [PATCH 14/14] Fix: fold release into 1.18.0 in CHANGELOG.md --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b47d216d..b7f9738198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Nothing should go in this section, please add to the latest unreleased version (and update the corresponding date), or add a new version. -## [1.18.1] - 2022-07-29 +## [1.18.0] - 2022-08-01 ### Added - Adds support for namespace label based identity scope for the Kubernetes Authenticator [cyberark/conjur#2613](https://github.com/cyberark/conjur/pull/2613) -## [1.18.0] - 2022-07-12 ### Changed - Adds support for authentication using OIDC's code authorization flow [cyberark/conjur#2595](https://github.com/cyberark/conjur/pull/2595)