Skip to content

Commit

Permalink
Publish servicebinding.io/v1 API
Browse files Browse the repository at this point in the history
servicebinding.io/v1beta1 remains supported. There is no difference in
functionality between v1 and v1beta1. v1beta1 is defined in the
semantics of v1, along with v1alpha3 (which remains deprecated, yet
functional).

We can choose to deprecate v1beta1 in the future.

Signed-off-by: Scott Andrews <[email protected]>
  • Loading branch information
scothis committed Feb 28, 2024
1 parent c43fd58 commit dfdc3b9
Show file tree
Hide file tree
Showing 66 changed files with 2,981 additions and 1,486 deletions.
20 changes: 20 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ resources:
webhooks:
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: servicebinding.io
kind: ServiceBinding
path: github.com/servicebinding/runtime/apis/v1
version: v1
webhooks:
validation: true
webhookVersion: v1
- api:
crdVersion: v1
domain: servicebinding.io
Expand All @@ -39,4 +50,13 @@ resources:
webhooks:
validation: true
webhookVersion: v1
- api:
crdVersion: v1
domain: servicebinding.io
kind: ClusterWorkloadResourceMapping
path: github.com/servicebinding/runtime/apis/v1
version: v1
webhooks:
validation: true
webhookVersion: v1
version: "3"
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![codecov](https://codecov.io/gh/servicebinding/runtime/branch/main/graph/badge.svg?token=D2Hs4MIXBZ)](https://codecov.io/gh/servicebinding/runtime)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

Reference implementation of the [ServiceBinding.io](https://servicebinding.io) [1.0 spec](https://servicebinding.io/spec/core/1.0.0/). The full specification is implemented, please open an issue for any discrepancies.
Reference implementation of the [ServiceBinding.io](https://servicebinding.io) [1.1 spec](https://servicebinding.io/spec/core/1.1.0/), along with [1.0](https://servicebinding.io/spec/core/1.0.0/). The full specification is implemented, please open an issue for any discrepancies.

- [Getting Started](#getting-started)
- [Deploy a released build](#deploy-a-released-build)
Expand Down Expand Up @@ -77,9 +77,9 @@ Samples are located in the [samples directory](./samples), including:

## Supported Services

Kubernetes defines no provisioned services by default, however, `Secret`s may be [directly referenced](https://servicebinding.io/spec/core/1.0.0/#direct-secret-reference).
Kubernetes defines no provisioned services by default, however, `Secret`s may be [directly referenced](https://servicebinding.io/spec/core/1.1.0/#direct-secret-reference).

Additional services can be supported dynamically by [defining a `ClusterRole`](https://servicebinding.io/spec/core/1.0.0/#considerations-for-role-based-access-control-rbac).
Additional services can be supported dynamically by [defining a `ClusterRole`](https://servicebinding.io/spec/core/1.1.0/#considerations-for-role-based-access-control-rbac).

## Supported Workloads

Expand All @@ -92,11 +92,11 @@ Support for the built-in k8s workload resource is pre-configured including:
- batch `Job` (since Jobs are immutable, the ServiceBinding must be defined and service resolved before the job is created)
- core `ReplicationController`

Additional workloads can be supported dynamically by [defining a `ClusterRole`](https://servicebinding.io/spec/core/1.0.0/#considerations-for-role-based-access-control-rbac-1) and if not PodSpecable, a [`ClusterWorkloadResourceMapping`](https://servicebinding.io/spec/core/1.0.0/#workload-resource-mapping).
Additional workloads can be supported dynamically by [defining a `ClusterRole`](https://servicebinding.io/spec/core/1.1.0/#considerations-for-role-based-access-control-rbac-1) and if not PodSpecable, a [`ClusterWorkloadResourceMapping`](https://servicebinding.io/spec/core/1.1.0/#workload-resource-mapping).

## Architecture

The [Service Binding for Kubernetes Specification](https://servicebinding.io/spec/core/1.0.0/) defines the shape of [Provisioned Services](https://servicebinding.io/spec/core/1.0.0/#provisioned-service), and how the `Secret` is [projected into a workload](https://servicebinding.io/spec/core/1.0.0/#workload-projection). The spec says less (intentionally) about how this happens.
The [Service Binding for Kubernetes Specification](https://servicebinding.io/spec/core/1.1.0/) defines the shape of [Provisioned Services](https://servicebinding.io/spec/core/1.1.0/#provisioned-service), and how the `Secret` is [projected into a workload](https://servicebinding.io/spec/core/1.1.0/#workload-projection). The spec says less (intentionally) about how this happens.

Both a controller and mutating admission webhook are used to project a `Secret` defined by the service referenced by the `ServiceBinding` resource into the workloads referenced. The controller is used to process `ServiceBinding`s by resolving services, projecting workloads and updating the status. The webhook is used to prevent removal of the workload projection, projecting workload on create, and a notification trigger for `ServiceBinding`s the controller should process.

Expand Down
24 changes: 24 additions & 0 deletions apis/v1/clusterworkloadresourcemapping_conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2023 Original Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package v1

import "sigs.k8s.io/controller-runtime/pkg/conversion"

var _ conversion.Hub = (*ClusterWorkloadResourceMapping)(nil)

// Hub for conversion
func (r *ClusterWorkloadResourceMapping) Hub() {}

Check warning on line 24 in apis/v1/clusterworkloadresourcemapping_conversion.go

View check run for this annotation

Codecov / codecov/patch

apis/v1/clusterworkloadresourcemapping_conversion.go#L24

Added line #L24 was not covered by tests
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1
package v1

import (
"testing"
Expand Down Expand Up @@ -121,7 +121,7 @@ func TestClusterWorkloadResourceMappingDefault(t *testing.T) {
Spec: ClusterWorkloadResourceMappingSpec{
Versions: []ClusterWorkloadResourceMappingTemplate{
{
Version: "v1beta1",
Version: "v1",
Annotations: ".metadata.annotations",
Containers: []ClusterWorkloadResourceMappingContainer{
{
Expand All @@ -145,7 +145,7 @@ func TestClusterWorkloadResourceMappingDefault(t *testing.T) {
Spec: ClusterWorkloadResourceMappingSpec{
Versions: []ClusterWorkloadResourceMappingTemplate{
{
Version: "v1beta1",
Version: "v1",
Annotations: ".metadata.annotations",
Containers: []ClusterWorkloadResourceMappingContainer{
{
Expand Down
91 changes: 91 additions & 0 deletions apis/v1/clusterworkloadresourcemapping_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2021 Original Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package v1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// ClusterWorkloadResourceMappingTemplate defines the mapping for a specific version of an workload resource to a
// logical PodTemplateSpec-like structure.
type ClusterWorkloadResourceMappingTemplate struct {
// Version is the version of the workload resource that this mapping is for.
Version string `json:"version"`
// Annotations is a Restricted JSONPath that references the annotations map within the workload resource. These
// annotations must end up in the resulting Pod, and are generally not the workload resource's annotations.
// Defaults to `.spec.template.metadata.annotations`.
Annotations string `json:"annotations,omitempty"`
// Containers is the collection of mappings to container-like fragments of the workload resource. Defaults to
// mappings appropriate for a PodSpecable resource.
Containers []ClusterWorkloadResourceMappingContainer `json:"containers,omitempty"`
// Volumes is a Restricted JSONPath that references the slice of volumes within the workload resource. Defaults to
// `.spec.template.spec.volumes`.
Volumes string `json:"volumes,omitempty"`
}

// ClusterWorkloadResourceMappingContainer defines the mapping for a specific fragment of an workload resource
// to a Container-like structure.
//
// Each mapping defines exactly one path that may match multiple container-like fragments within the workload
// resource. For each object matching the path the name, env and volumeMounts expressions are resolved to find those
// structures.
type ClusterWorkloadResourceMappingContainer struct {
// Path is the JSONPath within the workload resource that matches an existing fragment that is container-like.
Path string `json:"path"`
// Name is a Restricted JSONPath that references the name of the container with the container-like workload resource
// fragment. If not defined, container name filtering is ignored.
Name string `json:"name,omitempty"`
// Env is a Restricted JSONPath that references the slice of environment variables for the container with the
// container-like workload resource fragment. The referenced location is created if it does not exist. Defaults
// to `.envs`.
Env string `json:"env,omitempty"`
// VolumeMounts is a Restricted JSONPath that references the slice of volume mounts for the container with the
// container-like workload resource fragment. The referenced location is created if it does not exist. Defaults
// to `.volumeMounts`.
VolumeMounts string `json:"volumeMounts,omitempty"`
}

// ClusterWorkloadResourceMappingSpec defines the desired state of ClusterWorkloadResourceMapping
type ClusterWorkloadResourceMappingSpec struct {
// Versions is the collection of versions for a given resource, with mappings.
Versions []ClusterWorkloadResourceMappingTemplate `json:"versions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:storageversion
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// ClusterWorkloadResourceMapping is the Schema for the clusterworkloadresourcemappings API
type ClusterWorkloadResourceMapping struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec ClusterWorkloadResourceMappingSpec `json:"spec,omitempty"`
}

// +kubebuilder:object:root=true

// ClusterWorkloadResourceMappingList contains a list of ClusterWorkloadResourceMapping
type ClusterWorkloadResourceMappingList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`

Items []ClusterWorkloadResourceMapping `json:"items"`
}

func init() {
SchemeBuilder.Register(&ClusterWorkloadResourceMapping{}, &ClusterWorkloadResourceMappingList{})
}
181 changes: 181 additions & 0 deletions apis/v1/clusterworkloadresourcemapping_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
Copyright 2021 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1

import (
"fmt"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/client-go/util/jsonpath"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

func (r *ClusterWorkloadResourceMapping) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()

Check warning on line 33 in apis/v1/clusterworkloadresourcemapping_webhook.go

View check run for this annotation

Codecov / codecov/patch

apis/v1/clusterworkloadresourcemapping_webhook.go#L30-L33

Added lines #L30 - L33 were not covered by tests
}

var _ webhook.Defaulter = &ClusterWorkloadResourceMapping{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *ClusterWorkloadResourceMapping) Default() {
for i := range r.Spec.Versions {
r.Spec.Versions[i].Default()
}
}

// Default applies values that are appropriate for a PodSpecable resource
func (r *ClusterWorkloadResourceMappingTemplate) Default() {
if r.Annotations == "" {
r.Annotations = ".spec.template.metadata.annotations"
}
if len(r.Containers) == 0 {
r.Containers = []ClusterWorkloadResourceMappingContainer{
{
Path: ".spec.template.spec.initContainers[*]",
Name: ".name",
},
{
Path: ".spec.template.spec.containers[*]",
Name: ".name",
},
}
}
for i := range r.Containers {
c := &r.Containers[i]
if c.Env == "" {
c.Env = ".env"
}
if c.VolumeMounts == "" {
c.VolumeMounts = ".volumeMounts"
}
}
if r.Volumes == "" {
r.Volumes = ".spec.template.spec.volumes"
}
}

//+kubebuilder:webhook:path=/validate-servicebinding-io-v1-clusterworkloadresourcemapping,mutating=false,failurePolicy=fail,sideEffects=None,groups=servicebinding.io,resources=clusterworkloadresourcemappings,verbs=create;update,versions=v1,name=v1.clusterworkloadresourcemappings.servicebinding.io,admissionReviewVersions={v1,v1beta1}

var _ webhook.Validator = &ClusterWorkloadResourceMapping{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *ClusterWorkloadResourceMapping) ValidateCreate() (admission.Warnings, error) {
r.Default()
return nil, r.validate().ToAggregate()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *ClusterWorkloadResourceMapping) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
r.Default()
// TODO(user): check for immutable fields, if any
return nil, r.validate().ToAggregate()
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *ClusterWorkloadResourceMapping) ValidateDelete() (admission.Warnings, error) {
return nil, nil
}

func (r *ClusterWorkloadResourceMapping) validate() field.ErrorList {
errs := field.ErrorList{}

versions := map[string]int{}
for i := range r.Spec.Versions {
// check for duplicate versions
if p, ok := versions[r.Spec.Versions[i].Version]; ok {
errs = append(errs, field.Duplicate(field.NewPath("spec", "versions", fmt.Sprintf("[%d, %d]", p, i), "version"), r.Spec.Versions[i].Version))
}
versions[r.Spec.Versions[i].Version] = i
errs = append(errs, r.Spec.Versions[i].validate(field.NewPath("spec", "versions").Index(i))...)
}

return errs
}

func (r *ClusterWorkloadResourceMappingTemplate) validate(fldPath *field.Path) field.ErrorList {
errs := field.ErrorList{}

if r.Version == "" {
errs = append(errs, field.Required(fldPath.Child("version"), ""))
}
errs = append(errs, validateRestrictedJsonPath(r.Annotations, fldPath.Child("annotations"))...)
errs = append(errs, validateRestrictedJsonPath(r.Volumes, fldPath.Child("volumes"))...)
for i := range r.Containers {
errs = append(errs, r.Containers[i].validate(fldPath.Child("containers").Index(i))...)
}

return errs
}

func (r *ClusterWorkloadResourceMappingContainer) validate(fldPath *field.Path) field.ErrorList {
errs := field.ErrorList{}

errs = append(errs, validateJsonPath(r.Path, fldPath.Child("path"))...)
if r.Name != "" {
// name is optional
errs = append(errs, validateRestrictedJsonPath(r.Name, fldPath.Child("name"))...)
}
errs = append(errs, validateRestrictedJsonPath(r.Env, fldPath.Child("env"))...)
errs = append(errs, validateRestrictedJsonPath(r.VolumeMounts, fldPath.Child("volumeMounts"))...)

return errs
}

func validateJsonPath(expression string, fldPath *field.Path) field.ErrorList {
errs := field.ErrorList{}

if p, err := jsonpath.Parse("", fmt.Sprintf("{%s}", expression)); err != nil {
errs = append(errs, field.Invalid(fldPath, expression, err.Error()))
} else {
if len(p.Root.Nodes) != 1 {
errs = append(errs, field.Invalid(fldPath, expression, "too many root nodes"))
}
}

return errs
}

func validateRestrictedJsonPath(expression string, fldPath *field.Path) field.ErrorList {
errs := field.ErrorList{}

if p, err := jsonpath.Parse("", fmt.Sprintf("{%s}", expression)); err != nil {
errs = append(errs, field.Invalid(fldPath, expression, err.Error()))
} else {
if len(p.Root.Nodes) != 1 {
errs = append(errs, field.Invalid(fldPath, expression, "too many root nodes"))
}
// only allow jsonpath.NodeField nodes
nodes := p.Root.Nodes
for i := 0; i < len(nodes); i++ {
switch n := nodes[i].(type) {
case *jsonpath.ListNode:
nodes = append(nodes, n.Nodes...)
case *jsonpath.FieldNode:
continue
default:
errs = append(errs, field.Invalid(fldPath, expression, fmt.Sprintf("unsupported node: %s", n)))
}
}
}

return errs
}
Loading

0 comments on commit dfdc3b9

Please sign in to comment.