diff --git a/kubeflow/metacontroller/metacontroller.libsonnet b/kubeflow/metacontroller/metacontroller.libsonnet index aca9af4577b..bcc5c4169f7 100644 --- a/kubeflow/metacontroller/metacontroller.libsonnet +++ b/kubeflow/metacontroller/metacontroller.libsonnet @@ -135,7 +135,7 @@ "-v=4", "--discovery-interval=20s", ], - image: "metacontroller/metacontroller:0.2.0", + image: params.image, ports: [ { containerPort: 2345, diff --git a/kubeflow/metacontroller/prototypes/metacontroller.jsonnet b/kubeflow/metacontroller/prototypes/metacontroller.jsonnet index 977650c6b4f..139c31bae50 100644 --- a/kubeflow/metacontroller/prototypes/metacontroller.jsonnet +++ b/kubeflow/metacontroller/prototypes/metacontroller.jsonnet @@ -3,6 +3,7 @@ // @description metacontroller Component // @shortDescription metacontroller Component // @param name string Name +// @optionalParam image string metacontroller/metacontroller@sha256:f5af46268676e869b14dd54e37189ea3483ca27126f9f4425cf22ce7d7779a2d The metacontroller image local metacontroller = import "kubeflow/metacontroller/metacontroller.libsonnet"; local instance = metacontroller.new(env, params); diff --git a/kubeflow/metacontroller/tests/metacontroller_test.jsonnet b/kubeflow/metacontroller/tests/metacontroller_test.jsonnet index 305ea4d8544..d60071e193d 100644 --- a/kubeflow/metacontroller/tests/metacontroller_test.jsonnet +++ b/kubeflow/metacontroller/tests/metacontroller_test.jsonnet @@ -2,6 +2,7 @@ local metacontroller = import "kubeflow/metacontroller/metacontroller.libsonnet" local params = { name: "metacontroller", + image: "metacontroller/metacontroller@sha256:f5af46268676e869b14dd54e37189ea3483ca27126f9f4425cf22ce7d7779a2d", }; local env = { namespace: "kf-001", @@ -150,7 +151,7 @@ std.assertEqual( "-v=4", "--discovery-interval=20s", ], - image: "metacontroller/metacontroller:0.2.0", + image: "metacontroller/metacontroller@sha256:f5af46268676e869b14dd54e37189ea3483ca27126f9f4425cf22ce7d7779a2d", imagePullPolicy: "Always", name: "metacontroller", ports: [ diff --git a/kubeflow/profiles/parts.yaml b/kubeflow/profiles/parts.yaml new file mode 100644 index 00000000000..3322ab31fa0 --- /dev/null +++ b/kubeflow/profiles/parts.yaml @@ -0,0 +1,38 @@ +{ + "name": "profiles", + "apiVersion": "0.0.1", + "kind": "ksonnet.io/parts", + "description": "Profiles provide a way to build Targets.\n", + "author": "kubeflow team ", + "contributors": [ + { + "name": "Jeremy Lewi", + "email": "jlewi@google.com" + }, + { + "name": "Kam Kasravi", + "email": "kam.d.kasravi@intel.com" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/kubeflow/kubeflow" + }, + "bugs": { + "url": "https://github.com/kubeflow/kubeflow/issues" + }, + "keywords": [ + "kubeflow", + "profiles" + ], + "quickStart": { + "prototype": "io.ksonnet.pkg.profiles", + "componentName": "profiles", + "flags": { + "name": "profiles", + "namespace": "default" + }, + "comment": "Install profiles" + }, + "license": "Apache 2.0" +} diff --git a/kubeflow/profiles/profiles.libsonnet b/kubeflow/profiles/profiles.libsonnet new file mode 100644 index 00000000000..f72a8dde7d4 --- /dev/null +++ b/kubeflow/profiles/profiles.libsonnet @@ -0,0 +1,375 @@ +{ + local util = import "kubeflow/core/util.libsonnet", + + new(_env, _params):: { + local params = _env + _params, + + local profilesCRD = { + apiVersion: "apiextensions.k8s.io/v1beta1", + kind: "CustomResourceDefinition", + metadata: { + name: "profiles.kubeflow.org", + }, + spec: { + group: "kubeflow.org", + version: "v1alpha1", + scope: "Namespaced", + names: { + plural: "profiles", + singular: "profile", + kind: "Profile", + shortNames: [ + "prj", + ], + }, + validation: { + openAPIV3Schema: { + properties: { + apiVersion: { + type: "string", + }, + kind: { + type: "string", + }, + metadata: { + type: "object", + }, + spec: { + type: "object", + properties: { + selector: { + type: "object", + }, + template: { + type: "object", + properties: { + metadata: { + type: "object", + properties: { + namespace: { + type: "string", + }, + }, + }, + spec: { + type: "object", + properties: { + owner: { + type: "object", + required: [ + "kind", + "name", + ], + properties: { + apiGroup: { + type: "string", + }, + kind: { + enum: [ + "ServiceAccount", + "User", + ], + }, + namespace: { + type: "string", + }, + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + status: { + properties: { + observedGeneration: { + type: "int64", + }, + }, + type: "object", + }, + }, + }, + }, + }, + }, + profilesCRD:: profilesCRD, + + local permissionsCRD = { + apiVersion: "apiextensions.k8s.io/v1beta1", + kind: "CustomResourceDefinition", + metadata: { + name: "permissions.kubeflow.org", + }, + spec: { + group: "kubeflow.org", + version: "v1alpha1", + scope: "Namespaced", + names: { + plural: "permissions", + singular: "permission", + kind: "Permission", + }, + validation: { + openAPIV3Schema: { + properties: { + apiVersion: { + type: "string", + }, + kind: { + type: "string", + }, + metadata: { + type: "object", + }, + spec: { + type: "object", + properties: { + selector: { + type: "object", + }, + owner: { + type: "object", + required: [ + "kind", + "name", + ], + properties: { + apiGroup: { + type: "string", + }, + kind: { + enum: [ + "ServiceAccount", + "User", + ], + }, + namespace: { + type: "string", + }, + name: { + type: "string", + }, + }, + }, + }, + }, + status: { + properties: { + observedGeneration: { + type: "int64", + }, + }, + type: "object", + }, + }, + }, + }, + }, + }, + permissionsCRD:: permissionsCRD, + + local profilesService = { + apiVersion: "v1", + kind: "Service", + metadata: { + name: "profiles", + namespace: params.namespace, + }, + spec: { + selector: { + app: "profiles", + }, + ports: [ + { + port: 80, + targetPort: 8080, + }, + ], + }, + }, + profilesService:: profilesService, + + local profilesRole = { + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "Role", + metadata: { + name: "view", + namespace: params.namespace, + }, + rules: [ + { + apiGroups: [ + "kubeflow.org", + ], + resources: [ + "profiles", + ], + verbs: [ + "create", + ], + }, + { + apiGroups: [ + "kubeflow.org", + ], + resources: [ + "profiles", + ], + verbs: [ + "get", + ], + }, + ], + }, + profilesRole:: profilesRole, + + local profilesConfigMap = { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + name: "profiles", + namespace: params.namespace, + }, + data: { + "sync-profile.jsonnet": importstr "sync-profile.jsonnet", + "sync-permission.jsonnet": importstr "sync-permission.jsonnet", + }, + }, + profilesConfigMap:: profilesConfigMap, + + local profilesDeployment = { + apiVersion: "apps/v1", + kind: "Deployment", + metadata: { + name: "profiles", + namespace: params.namespace, + }, + spec: { + selector: { + matchLabels: { + app: "profiles", + }, + }, + template: { + metadata: { + labels: { + app: "profiles", + }, + }, + spec: { + containers: [ + { + name: "hooks", + //freeze latest + image: params.image, + imagePullPolicy: "Always", + workingDir: "/opt/profiles/hooks", + volumeMounts: [ + { + name: "hooks", + mountPath: "/opt/profiles/hooks", + }, + ], + }, + ], + volumes: [ + { + name: "hooks", + configMap: { + name: "profiles", + }, + }, + ], + }, + }, + }, + }, + profilesDeployment:: profilesDeployment, + + local profilesController = { + apiVersion: "metacontroller.k8s.io/v1alpha1", + kind: "CompositeController", + metadata: { + name: "profiles-controller", + }, + spec: { + generateSelector: true, + parentResource: { + apiVersion: "kubeflow.org/v1alpha1", + resource: "profiles", + }, + childResources: [ + { + apiVersion: "v1", + resource: "namespaces", + }, + { + apiVersion: "kubeflow.org/v1alpha1", + resource: "permissions", + }, + ], + hooks: { + sync: { + webhook: { + url: "http://profiles." + params.namespace + "/sync-profile", + }, + }, + }, + }, + }, + profilesController:: profilesController, + + local permissionsController = { + apiVersion: "metacontroller.k8s.io/v1alpha1", + kind: "CompositeController", + metadata: { + name: "permissions-controller", + }, + spec: { + generateSelector: true, + parentResource: { + apiVersion: "kubeflow.org/v1alpha1", + resource: "permissions", + }, + childResources: [ + { + apiVersion: "rbac.authorization.k8s.io/v1", + resource: "roles", + }, + { + apiVersion: "rbac.authorization.k8s.io/v1", + resource: "rolebindings", + }, + ], + hooks: { + sync: { + webhook: { + url: "http://profiles." + params.namespace + "/sync-permission", + }, + }, + }, + }, + }, + permissionsController:: permissionsController, + + parts:: self, + local all = [ + self.profilesCRD, + self.permissionsCRD, + self.profilesService, + self.profilesRole, + self.profilesConfigMap, + self.profilesDeployment, + self.profilesController, + self.permissionsController, + ], + all:: all, + + list(obj=self.all):: util.list(obj), + }, +} diff --git a/kubeflow/profiles/prototypes/README.md b/kubeflow/profiles/prototypes/README.md new file mode 100644 index 00000000000..5d8acc31eab --- /dev/null +++ b/kubeflow/profiles/prototypes/README.md @@ -0,0 +1,151 @@ +## Goals + +- Provide a self-serve environment for data-scientists to create one or more protected namespaces where +notebooks, jobs, and other components can be deployed and run in this namespace. + +- Use native kubernetes RBAC rules to isolate this namespace to a particular user's service account. + +- Do not grant cluster wide privileges to a user when creating a protected namespace. + +- Only use namespaced scoped Roles and RoleBindings. + +- Separate infra components from user components where intra components reside in a shared/admin namespace and user components reside in protected namespaces. + +- Allow a user to be either a ServiceAccount in the shared/admin namespace or a User which can map to a GKE IAM role. + +- Enable a forward path to include proposed [Security Profiles](https://github.com/kubernetes/community/blob/a8cb2060dc621664c86b185c7426367994b181b5/keps/draft-20180418-security-profile.md) + + +## Design + +Protected Namespaces allow a data scientist to use shared kubeflow components but within a namespace that is protected. + +![shared and protected namespaces](./docs/namespaces.png "shared and protected namespaces") + +Users __stan__ and __jackie__ are able to run notebooks, jobs, and other components within their own protected namespace. + +![jobs and notebooks](./docs/jobsandnotebooks.png "jobs and notebooks") + +Users __stan__ and __jackie__ are subjects within the shared namespace. A subject can be either a User (for GKE a user can be their IAM role where their name is their email address) or a ServiceAccount. If they are ServiceAccounts, these may be created by the kubeflow admin within the kubeflow namespace and the secret tokens for each ServiceAccount distributed to each user and appended to their $HOME/.kube/config files. + +- Service Account + +![service accounts](./docs/serviceaccounts.png "service accounts") + +- User + +![iam users](./docs/iamusers.png "IAM users") + +Per user, the kubeflow admin creates a RoleBinding for that user in the shared namespace. The RoleBinding's roleRef is a constained Role that only allows the user to create and get Profile CRs. The subject is - as noted above - a ServiceAccount or User. + +![rolebindings](./docs/rolebindings.png "rolebindings") + +For __stan__ the RoleBinding may look the following + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: stan + namespace: kubeflow +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: view +subjects: +- kind: ServiceAccount + name: stan + namespace: kubeflow +``` + +The __view__ Role that __stan__ has (shown above) in the kubeflow shared namespace is: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: view + namespace: kubeflow +rules: +- apiGroups: + - kubeflow.org + resources: + - profiles + verbs: + - create +- apiGroups: + - kubeflow.org + resources: + - profiles + verbs: + - get +``` + +This means that users have very few privileges within the shared namespace, limited to creating and getting a Profile CR. Besides the Profile CRD, there is one additional Custom Resource Definition used implement protected namespaces called a Permissions CRD. In total - the CRDs are: + +- Profile +- Permissions + +Both custom resources have an associated controllers which do the following: + +- profiles-controller + - watches for __Profile__ Custom Resources in the kubeflow namespace + - creates a Namespace and Permission Resource +- permissions-controller + - watches for __Permission__ Custom Resources in any protected namespace + - creates a Role and RoleBinding Resource + + +The user flow is as follows: + +![userflow](./docs/userflow.png "userflow") + +The controllers are namescoped and watch / create resources in different namespaces shown below: + +![namespace groups](./docs/namespacegroups.png "namespace groups") + + +### Data Structures + +The Profile resource contains a template section where a namespace and owner are specified. The Profile resource is created within the shared namespace. An example is: + +```yaml +apiVersion: kubeflow.org/v1alpha1 +kind: Profile +metadata: + name: gan + namespace: kubeflow +spec: + template: + metadata: + namespace: gan + spec: + owner: + kind: User + apiGroup: rbac.authorization.k8s.io + name: alice@foo.com +``` + +The Permission resource contains the RBAC Role, RoleBinding that will be created for the user within the protected namespace. The Permission resource is also created within the protected namespace. An example is: + +```yaml +apiVersion: kubeflow.org/v1alpha1 +kind: Permission +metadata: + labels: + controller-uid: cc6cf46d-d9ea-11e8-9846-42010a8a00a5 + name: default + namespace: gan + ownerReferences: + - apiVersion: kubeflow.org/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Profile + name: gan +spec: + owner: + kind: User + apiGroup: rbac.authorization.k8s.io + name: alice@foo.com +``` + diff --git a/kubeflow/profiles/prototypes/docs/iamusers.png b/kubeflow/profiles/prototypes/docs/iamusers.png new file mode 100644 index 00000000000..c50ab09ea28 Binary files /dev/null and b/kubeflow/profiles/prototypes/docs/iamusers.png differ diff --git a/kubeflow/profiles/prototypes/docs/jobsandnotebooks.png b/kubeflow/profiles/prototypes/docs/jobsandnotebooks.png new file mode 100644 index 00000000000..e880331c1ed Binary files /dev/null and b/kubeflow/profiles/prototypes/docs/jobsandnotebooks.png differ diff --git a/kubeflow/profiles/prototypes/docs/namespacegroups.png b/kubeflow/profiles/prototypes/docs/namespacegroups.png new file mode 100644 index 00000000000..553acd116d6 Binary files /dev/null and b/kubeflow/profiles/prototypes/docs/namespacegroups.png differ diff --git a/kubeflow/profiles/prototypes/docs/namespaces.png b/kubeflow/profiles/prototypes/docs/namespaces.png new file mode 100644 index 00000000000..8fb6678f1a0 Binary files /dev/null and b/kubeflow/profiles/prototypes/docs/namespaces.png differ diff --git a/kubeflow/profiles/prototypes/docs/rolebindings.png b/kubeflow/profiles/prototypes/docs/rolebindings.png new file mode 100644 index 00000000000..2730546cddf Binary files /dev/null and b/kubeflow/profiles/prototypes/docs/rolebindings.png differ diff --git a/kubeflow/profiles/prototypes/docs/serviceaccounts.png b/kubeflow/profiles/prototypes/docs/serviceaccounts.png new file mode 100644 index 00000000000..a70102f1387 Binary files /dev/null and b/kubeflow/profiles/prototypes/docs/serviceaccounts.png differ diff --git a/kubeflow/profiles/prototypes/docs/userflow.png b/kubeflow/profiles/prototypes/docs/userflow.png new file mode 100644 index 00000000000..b62e94ce515 Binary files /dev/null and b/kubeflow/profiles/prototypes/docs/userflow.png differ diff --git a/kubeflow/profiles/prototypes/profiles.jsonnet b/kubeflow/profiles/prototypes/profiles.jsonnet new file mode 100644 index 00000000000..accf3989875 --- /dev/null +++ b/kubeflow/profiles/prototypes/profiles.jsonnet @@ -0,0 +1,10 @@ +// @apiVersion 0.1 +// @name io.ksonnet.pkg.profiles +// @description profiles Component +// @shortDescription profiles Component +// @param name string Name +// @optionalParam image string metacontroller/jsonnetd@sha256:25c25f217ad030a0f67e37078c33194785b494569b0c088d8df4f00da8fd15a0 The image to use for jsonnet + +local profile = import "kubeflow/profiles/profiles.libsonnet"; +local instance = profile.new(env, params); +instance.list(instance.all) diff --git a/kubeflow/profiles/sync-permission.jsonnet b/kubeflow/profiles/sync-permission.jsonnet new file mode 100644 index 00000000000..c866e9cd1cf --- /dev/null +++ b/kubeflow/profiles/sync-permission.jsonnet @@ -0,0 +1,300 @@ +// Controller for resource: permissions +// Creates 2 child resources +// - Role +// - RoleBinding +function(request) { + local apiVersion = "kubeflow.org/v1alpha1", + local template = request.parent.spec.template, + local children = [ + { + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "Role", + metadata: { + name: "edit", + namespace: request.parent.metadata.namespace, + }, + rules: [ + { + apiGroups: [ + "metacontroller.k8s.io", + ], + resources: [ + "compositecontrollers", + "decoratecontrollers", + ], + verbs: [ + "create", + "delete", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "kubeflow.org", + ], + resources: [ + "profiles", + "permissions", + "notebooks", + ], + verbs: [ + "create", + "delete", + "get", + "list", + "watch", + ], + }, + { + apiGroups: [ + "app.k8s.io", + ], + resources: [ + "applications", + "apps", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "", + ], + resources: [ + "pods", + "pods/attach", + "pods/exec", + "pods/portforward", + "pods/proxy", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "", + ], + resources: [ + "configmaps", + "endpoints", + "persistentvolumeclaims", + "replicationcontrollers", + "replicationcontrollers/scale", + "secrets", + "serviceaccounts", + "services", + "services/proxy", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "", + ], + resources: [ + "bindings", + "events", + "limitranges", + "pods/log", + "pods/status", + "replicationcontrollers/status", + "resourcequotas", + "resourcequotas/status", + ], + verbs: [ + "get", + "list", + "watch", + ], + }, + { + apiGroups: [ + "", + ], + resources: [ + "serviceaccounts", + ], + verbs: [ + "impersonate", + ], + }, + { + apiGroups: [ + "apps", + ], + resources: [ + "daemonsets", + "deployments", + "deployments/rollback", + "deployments/scale", + "replicasets", + "replicasets/scale", + "statefulsets", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "autoscaling", + ], + resources: [ + "horizontalpodautoscalers", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "batch", + ], + resources: [ + "cronjobs", + "jobs", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "extensions", + ], + resources: [ + "daemonsets", + "deployments", + "deployments/rollback", + "deployments/scale", + "ingresses", + "networkpolicies", + "replicasets", + "replicasets/scale", + "replicationcontrollers/scale", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "policy", + ], + resources: [ + "poddisruptionbudgets", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "networking.k8s.io", + ], + resources: [ + "networkpolicies", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + ], + }, + { + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "RoleBinding", + metadata: { + name: "default", + namespace: request.parent.metadata.namespace, + }, + roleRef: { + apiGroup: "rbac.authorization.k8s.io", + kind: "Role", + name: "edit", + }, + subjects: [ + request.parent.spec.owner, + ], + }, + ], + children: children, + status: { + phase: "Active", + conditions: [{ + type: "Ready", + }], + created: true, + }, +} diff --git a/kubeflow/profiles/sync-profile.jsonnet b/kubeflow/profiles/sync-profile.jsonnet new file mode 100644 index 00000000000..e01567ff8ea --- /dev/null +++ b/kubeflow/profiles/sync-profile.jsonnet @@ -0,0 +1,36 @@ +// Controller for resource: profiles +// Creates 2 child resources +// - Namespace +// - Permission +function(request) { + local apiVersion = "kubeflow.org/v1alpha1", + local template = request.parent.spec.template, + local children = [ + { + apiVersion: "v1", + kind: "Namespace", + metadata: { + name: template.metadata.namespace, + }, + }, + { + apiVersion: "kubeflow.org/v1alpha1", + kind: "Permission", + metadata: { + name: "default", + namespace: template.metadata.namespace, + }, + spec: { + owner: template.spec.owner, + }, + }, + ], + children: children, + status: { + phase: "Active", + conditions: [{ + type: "Ready", + }], + created: true, + }, +} diff --git a/kubeflow/profiles/tests/profiles_test.jsonnet b/kubeflow/profiles/tests/profiles_test.jsonnet new file mode 100644 index 00000000000..7e3507f88e1 --- /dev/null +++ b/kubeflow/profiles/tests/profiles_test.jsonnet @@ -0,0 +1,379 @@ +local profiles = import "kubeflow/profiles/profiles.libsonnet"; + +local params = { + name: "profiles", + image: "metacontroller/jsonnetd@sha256:25c25f217ad030a0f67e37078c33194785b494569b0c088d8df4f00da8fd15a0", +}; +local env = { + namespace: "kf-001", +}; + +local instance = profiles.new(env, params); + +std.assertEqual( + instance.parts.profilesCRD, + { + apiVersion: "apiextensions.k8s.io/v1beta1", + kind: "CustomResourceDefinition", + metadata: { + name: "profiles.kubeflow.org", + }, + spec: { + group: "kubeflow.org", + names: { + kind: "Profile", + plural: "profiles", + shortNames: [ + "prj", + ], + singular: "profile", + }, + scope: "Namespaced", + validation: { + openAPIV3Schema: { + properties: { + apiVersion: { + type: "string", + }, + kind: { + type: "string", + }, + metadata: { + type: "object", + }, + spec: { + properties: { + selector: { + type: "object", + }, + template: { + properties: { + metadata: { + properties: { + namespace: { + type: "string", + }, + }, + type: "object", + }, + spec: { + properties: { + owner: { + properties: { + apiGroup: { + type: "string", + }, + kind: { + enum: [ + "ServiceAccount", + "User", + ], + }, + name: { + type: "string", + }, + namespace: { + type: "string", + }, + }, + required: [ + "kind", + "name", + ], + type: "object", + }, + }, + type: "object", + }, + }, + type: "object", + }, + }, + type: "object", + }, + status: { + properties: { + observedGeneration: { + type: "int64", + }, + }, + type: "object", + }, + }, + }, + }, + version: "v1alpha1", + }, + } +) && + +std.assertEqual( + instance.parts.permissionsCRD, + { + apiVersion: "apiextensions.k8s.io/v1beta1", + kind: "CustomResourceDefinition", + metadata: { + name: "permissions.kubeflow.org", + }, + spec: { + group: "kubeflow.org", + names: { + kind: "Permission", + plural: "permissions", + singular: "permission", + }, + scope: "Namespaced", + validation: { + openAPIV3Schema: { + properties: { + apiVersion: { + type: "string", + }, + kind: { + type: "string", + }, + metadata: { + type: "object", + }, + spec: { + properties: { + owner: { + properties: { + apiGroup: { + type: "string", + }, + kind: { + enum: [ + "ServiceAccount", + "User", + ], + }, + name: { + type: "string", + }, + namespace: { + type: "string", + }, + }, + required: [ + "kind", + "name", + ], + type: "object", + }, + selector: { + type: "object", + }, + }, + type: "object", + }, + status: { + properties: { + observedGeneration: { + type: "int64", + }, + }, + type: "object", + }, + }, + }, + }, + version: "v1alpha1", + }, + } +) && + +std.assertEqual( + instance.parts.profilesService, + { + apiVersion: "v1", + kind: "Service", + metadata: { + name: "profiles", + namespace: "kf-001", + }, + spec: { + ports: [ + { + port: 80, + targetPort: 8080, + }, + ], + selector: { + app: "profiles", + }, + }, + } +) && + +std.assertEqual( + instance.parts.profilesRole, + { + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "Role", + metadata: { + name: "view", + namespace: "kf-001", + }, + rules: [ + { + apiGroups: [ + "kubeflow.org", + ], + resources: [ + "profiles", + ], + verbs: [ + "create", + ], + }, + { + apiGroups: [ + "kubeflow.org", + ], + resources: [ + "profiles", + ], + verbs: [ + "get", + ], + }, + ], + } +) && + +std.assertEqual( + instance.parts.profilesConfigMap, + { + apiVersion: "v1", + data: { + "sync-permission.jsonnet": importstr "../sync-permission.jsonnet", + "sync-profile.jsonnet": importstr "../sync-profile.jsonnet", + }, + kind: "ConfigMap", + metadata: { + name: "profiles", + namespace: "kf-001", + }, + } +) && + +std.assertEqual( + instance.parts.profilesDeployment, + { + apiVersion: "apps/v1", + kind: "Deployment", + metadata: { + name: "profiles", + namespace: "kf-001", + }, + spec: { + selector: { + matchLabels: { + app: "profiles", + }, + }, + template: { + metadata: { + labels: { + app: "profiles", + }, + }, + spec: { + containers: [ + { + image: "metacontroller/jsonnetd@sha256:25c25f217ad030a0f67e37078c33194785b494569b0c088d8df4f00da8fd15a0", + imagePullPolicy: "Always", + name: "hooks", + volumeMounts: [ + { + mountPath: "/opt/profiles/hooks", + name: "hooks", + }, + ], + workingDir: "/opt/profiles/hooks", + }, + ], + volumes: [ + { + configMap: { + name: "profiles", + }, + name: "hooks", + }, + ], + }, + }, + }, + } +) && + +std.assertEqual( + instance.parts.profilesController, + { + apiVersion: "metacontroller.k8s.io/v1alpha1", + kind: "CompositeController", + metadata: { + name: "profiles-controller", + }, + spec: { + childResources: [ + { + apiVersion: "v1", + resource: "namespaces", + }, + { + apiVersion: "kubeflow.org/v1alpha1", + resource: "permissions", + }, + ], + generateSelector: true, + hooks: { + sync: { + webhook: { + url: "http://profiles.kf-001/sync-profile", + }, + }, + }, + parentResource: { + apiVersion: "kubeflow.org/v1alpha1", + resource: "profiles", + }, + }, + } +) && + +std.assertEqual( + instance.parts.permissionsController, + { + apiVersion: "metacontroller.k8s.io/v1alpha1", + kind: "CompositeController", + metadata: { + name: "permissions-controller", + }, + spec: { + childResources: [ + { + apiVersion: "rbac.authorization.k8s.io/v1", + resource: "roles", + }, + { + apiVersion: "rbac.authorization.k8s.io/v1", + resource: "rolebindings", + }, + ], + generateSelector: true, + hooks: { + sync: { + webhook: { + url: "http://profiles.kf-001/sync-permission", + }, + }, + }, + parentResource: { + apiVersion: "kubeflow.org/v1alpha1", + resource: "permissions", + }, + }, + } +) diff --git a/kubeflow/profiles/tests/sync-permission_test.jsonnet b/kubeflow/profiles/tests/sync-permission_test.jsonnet new file mode 100644 index 00000000000..3495d949b7c --- /dev/null +++ b/kubeflow/profiles/tests/sync-permission_test.jsonnet @@ -0,0 +1,327 @@ +local params = { + user: "chloe", +}; + +local env = { + namespace: "kubeflow", +}; + +local request = { + parent: { + apiVersion: "kubeflow.org/v1alpha1", + kind: "Permission", + metadata: { + name: "default", + namespace: env.namespace, + }, + spec: { + owner: params.user, + serviceAccountNamespace: env.namespace, + }, + }, + children: { + "Role.rbac.authorization.k8s.io/v1": {}, + "RoleBindings.rbac.authorization.k8s.io/v1": {}, + }, +}; + +local syncPermission = import "kubeflow/profiles/sync-permission.jsonnet"; + +std.assertEqual( + syncPermission(request), + { + children: [ + { + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "Role", + metadata: { + name: "edit", + namespace: "kubeflow", + }, + rules: [ + { + apiGroups: [ + "metacontroller.k8s.io", + ], + resources: [ + "compositecontrollers", + "decoratecontrollers", + ], + verbs: [ + "create", + "delete", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "kubeflow.org", + ], + resources: [ + "profiles", + "permissions", + "notebooks", + ], + verbs: [ + "create", + "delete", + "get", + "list", + "watch", + ], + }, + { + apiGroups: [ + "app.k8s.io", + ], + resources: [ + "applications", + "apps", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "", + ], + resources: [ + "pods", + "pods/attach", + "pods/exec", + "pods/portforward", + "pods/proxy", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "", + ], + resources: [ + "configmaps", + "endpoints", + "persistentvolumeclaims", + "replicationcontrollers", + "replicationcontrollers/scale", + "secrets", + "serviceaccounts", + "services", + "services/proxy", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "", + ], + resources: [ + "bindings", + "events", + "limitranges", + "pods/log", + "pods/status", + "replicationcontrollers/status", + "resourcequotas", + "resourcequotas/status", + ], + verbs: [ + "get", + "list", + "watch", + ], + }, + { + apiGroups: [ + "", + ], + resources: [ + "serviceaccounts", + ], + verbs: [ + "impersonate", + ], + }, + { + apiGroups: [ + "apps", + ], + resources: [ + "daemonsets", + "deployments", + "deployments/rollback", + "deployments/scale", + "replicasets", + "replicasets/scale", + "statefulsets", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "autoscaling", + ], + resources: [ + "horizontalpodautoscalers", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "batch", + ], + resources: [ + "cronjobs", + "jobs", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "extensions", + ], + resources: [ + "daemonsets", + "deployments", + "deployments/rollback", + "deployments/scale", + "ingresses", + "networkpolicies", + "replicasets", + "replicasets/scale", + "replicationcontrollers/scale", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "policy", + ], + resources: [ + "poddisruptionbudgets", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + { + apiGroups: [ + "networking.k8s.io", + ], + resources: [ + "networkpolicies", + ], + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + }, + ], + }, + { + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "RoleBinding", + metadata: { + name: "default", + namespace: "kubeflow", + }, + roleRef: { + apiGroup: "rbac.authorization.k8s.io", + kind: "Role", + name: "edit", + }, + subjects: [ + "chloe", + ], + }, + ], + status: { + conditions: [ + { + type: "Ready", + }, + ], + created: true, + phase: "Active", + }, + } +) diff --git a/kubeflow/profiles/tests/sync-profile_test.jsonnet b/kubeflow/profiles/tests/sync-profile_test.jsonnet new file mode 100644 index 00000000000..49b91779d13 --- /dev/null +++ b/kubeflow/profiles/tests/sync-profile_test.jsonnet @@ -0,0 +1,149 @@ +local params = { + users: { + stan: { + kind: "ServiceAccount", + name: "stan", + namespace: "kubeflow", + }, + jackie: { + kind: "User", + name: "jackie", + apiGroup: "rbac.authorization.k8s.io", + }, + }, + protectedNamespace: "iris", +}; + +local env = { + namespace: "kubeflow", +}; + +local syncProfile = import "kubeflow/profiles/sync-profile.jsonnet"; + +local request1 = { + parent: { + apiVersion: "kubeflow.org/v1alpha1", + kind: "Profile", + metadata: { + name: params.protectedNamespace, + namespace: env.namespace, + }, + spec: { + template: { + metadata: { + namespace: params.protectedNamespace, + }, + spec: { + owner: params.users.stan, + }, + }, + }, + }, + children: { + "Namespace.v1": {}, + "Permission.kubeflow.org/v1alpha1": {}, + }, +}; + +local request2 = { + parent: { + apiVersion: "kubeflow.org/v1alpha1", + kind: "Profile", + metadata: { + name: params.protectedNamespace, + namespace: env.namespace, + }, + spec: { + template: { + metadata: { + namespace: params.protectedNamespace, + }, + spec: { + owner: params.users.jackie, + }, + }, + }, + }, + children: { + "Namespace.v1": {}, + "Permission.kubeflow.org/v1alpha1": {}, + }, +}; + +std.assertEqual( + syncProfile(request1), + { + children: [ + { + apiVersion: "v1", + kind: "Namespace", + metadata: { + name: "iris", + }, + }, + { + apiVersion: "kubeflow.org/v1alpha1", + kind: "Permission", + metadata: { + name: "default", + namespace: "iris", + }, + spec: { + owner: { + kind: "ServiceAccount", + name: "stan", + namespace: "kubeflow", + }, + }, + }, + ], + status: { + conditions: [ + { + type: "Ready", + }, + ], + created: true, + phase: "Active", + }, + } +) && + +std.assertEqual( + syncProfile(request2), + { + children: [ + { + apiVersion: "v1", + kind: "Namespace", + metadata: { + name: "iris", + }, + }, + { + apiVersion: "kubeflow.org/v1alpha1", + kind: "Permission", + metadata: { + name: "default", + namespace: "iris", + }, + spec: { + owner: { + apiGroup: "rbac.authorization.k8s.io", + kind: "User", + name: "jackie", + }, + }, + }, + ], + status: { + conditions: [ + { + type: "Ready", + }, + ], + created: true, + phase: "Active", + }, + } +) diff --git a/kubeflow/registry.yaml b/kubeflow/registry.yaml index 6d9df37e097..eb9579b5dcb 100644 --- a/kubeflow/registry.yaml +++ b/kubeflow/registry.yaml @@ -4,6 +4,12 @@ libraries: application: version: master path: application + metacontroller: + version: master + path: metacontroller + profiles: + version: master + path: profiles core: version: master path: core diff --git a/scripts/util.sh b/scripts/util.sh index 4f4db679bcf..b90fec7e1ff 100644 --- a/scripts/util.sh +++ b/scripts/util.sh @@ -57,6 +57,7 @@ function createKsApp() { ks pkg install kubeflow/seldon ks pkg install kubeflow/tf-serving ks pkg install kubeflow/metacontroller + ks pkg install kubeflow/profiles ks pkg install kubeflow/application # Generate all required components @@ -67,6 +68,7 @@ function createKsApp() { ks generate centraldashboard centraldashboard ks generate tf-job-operator tf-job-operator ks generate metacontroller metacontroller + ks generate profiles profiles ks generate argo argo ks generate katib katib diff --git a/testing/test_jsonnet.py b/testing/test_jsonnet.py index d4181f470c1..1211a1b7021 100644 --- a/testing/test_jsonnet.py +++ b/testing/test_jsonnet.py @@ -22,7 +22,7 @@ Example invocation -python python -m testing.test_jsonnet --test_files_dirs=/kubeflow/core/tests,/kubeflow/iap/tests,/kubeflow/tensorboard/tests,/kubeflow/examples/tests,/kubeflow/metacontroller/tests --artifacts_dir=/tmp/artifacts +python python -m testing.test_jsonnet --test_files_dirs=/kubeflow/core/tests,/kubeflow/iap/tests,/kubeflow/tensorboard/tests,/kubeflow/examples/tests,/kubeflow/metacontroller/tests,/kubeflow/profiles/tests --artifacts_dir=/tmp/artifacts """ diff --git a/testing/workflows/components/unit_tests.jsonnet b/testing/workflows/components/unit_tests.jsonnet index c1361a32de6..5c3039ad255 100644 --- a/testing/workflows/components/unit_tests.jsonnet +++ b/testing/workflows/components/unit_tests.jsonnet @@ -112,7 +112,12 @@ local dagTemplates = [ "-m", "testing.test_jsonnet", "--artifacts_dir=" + artifactsDir, - "--test_files_dirs=" + srcDir + "/kubeflow", + "--test_files_dirs=" + srcDir + "/kubeflow/application/tests" + "," + + srcDir + "/kubeflow/core/tests" + "," + + srcDir + "/kubeflow/examples/tests" + "," + + srcDir + "/kubeflow/metacontroller/tests" + "," + + srcDir + "/kubeflow/profiles/tests" + "," + + srcDir + "/kubeflow/tensorboard/tests", "--jsonnet_path_dirs=" + srcDir + "," + srcRootDir + "/kubeflow/testing/workflows/lib/v1.7.0/", ]), // jsonnet-test