knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/duck/v1/source_types.go (about)

     1  /*
     2  Copyright 2019 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package v1
    18  
    19  import (
    20  	"context"
    21  	"time"
    22  
    23  	corev1 "k8s.io/api/core/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  
    27  	"knative.dev/pkg/apis"
    28  	"knative.dev/pkg/apis/duck/ducktypes"
    29  )
    30  
    31  // +genduck
    32  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    33  
    34  // Source is the minimum resource shape to adhere to the Source Specification.
    35  // This duck type is intended to allow implementors of Sources and
    36  // Importers to verify their own resources meet the expectations.
    37  // This is not a real resource.
    38  // NOTE: The Source Specification is in progress and the shape and names could
    39  // be modified until it has been accepted.
    40  type Source struct {
    41  	metav1.TypeMeta   `json:",inline"`
    42  	metav1.ObjectMeta `json:"metadata,omitempty"`
    43  
    44  	Spec   SourceSpec   `json:"spec"`
    45  	Status SourceStatus `json:"status"`
    46  }
    47  
    48  type SourceSpec struct {
    49  	// Sink is a reference to an object that will resolve to a uri to use as the sink.
    50  	Sink Destination `json:"sink,omitempty"`
    51  
    52  	// CloudEventOverrides defines overrides to control the output format and
    53  	// modifications of the event sent to the sink.
    54  	// +optional
    55  	CloudEventOverrides *CloudEventOverrides `json:"ceOverrides,omitempty"`
    56  }
    57  
    58  // CloudEventOverrides defines arguments for a Source that control the output
    59  // format of the CloudEvents produced by the Source.
    60  type CloudEventOverrides struct {
    61  	// Extensions specify what attribute are added or overridden on the
    62  	// outbound event. Each `Extensions` key-value pair are set on the event as
    63  	// an attribute extension independently.
    64  	// +optional
    65  	Extensions map[string]string `json:"extensions,omitempty"`
    66  }
    67  
    68  // SourceStatus shows how we expect folks to embed Addressable in
    69  // their Status field.
    70  type SourceStatus struct {
    71  	// inherits Status, which currently provides:
    72  	// * ObservedGeneration - the 'Generation' of the Service that was last
    73  	//   processed by the controller.
    74  	// * Conditions - the latest available observations of a resource's current
    75  	//   state.
    76  	Status `json:",inline"`
    77  
    78  	// SinkURI is the current active sink URI that has been configured for the
    79  	// Source.
    80  	// +optional
    81  	SinkURI *apis.URL `json:"sinkUri,omitempty"`
    82  
    83  	// CloudEventAttributes are the specific attributes that the Source uses
    84  	// as part of its CloudEvents.
    85  	// +optional
    86  	CloudEventAttributes []CloudEventAttributes `json:"ceAttributes,omitempty"`
    87  
    88  	// SinkCACerts are Certification Authority (CA) certificates in PEM format
    89  	// according to https://www.rfc-editor.org/rfc/rfc7468.
    90  	// +optional
    91  	SinkCACerts *string `json:"sinkCACerts,omitempty"`
    92  
    93  	// SinkAudience is the OIDC audience of the sink.
    94  	// +optional
    95  	SinkAudience *string `json:"sinkAudience,omitempty"`
    96  
    97  	// Auth defines the attributes that provide the generated service account
    98  	// name in the resource status.
    99  	// +optional
   100  	Auth *AuthStatus `json:"auth,omitempty"`
   101  }
   102  
   103  // CloudEventAttributes specifies the attributes that a Source
   104  // uses as part of its CloudEvents.
   105  type CloudEventAttributes struct {
   106  	// Type refers to the CloudEvent type attribute.
   107  	Type string `json:"type,omitempty"`
   108  
   109  	// Source is the CloudEvents source attribute.
   110  	Source string `json:"source,omitempty"`
   111  }
   112  
   113  // IsReady returns true if the resource is ready overall.
   114  func (ss *SourceStatus) IsReady() bool {
   115  	for _, c := range ss.Conditions {
   116  		switch c.Type {
   117  		// Look for the "happy" condition, which is the only condition that
   118  		// we can reliably understand to be the overall state of the resource.
   119  		case apis.ConditionReady, apis.ConditionSucceeded:
   120  			return c.IsTrue()
   121  		}
   122  	}
   123  	return false
   124  }
   125  
   126  // Verify Source resources meet duck contracts.
   127  var (
   128  	_ apis.Listable           = (*Source)(nil)
   129  	_ ducktypes.Implementable = (*Source)(nil)
   130  	_ ducktypes.Populatable   = (*Source)(nil)
   131  )
   132  
   133  const (
   134  	// SourceConditionSinkProvided has status True when the Source
   135  	// has been configured with a sink target that is resolvable.
   136  	SourceConditionSinkProvided apis.ConditionType = "SinkProvided"
   137  )
   138  
   139  // GetFullType implements duck.Implementable
   140  func (*Source) GetFullType() ducktypes.Populatable {
   141  	return &Source{}
   142  }
   143  
   144  // Populate implements duck.Populatable
   145  func (s *Source) Populate() {
   146  	s.Spec.Sink = Destination{
   147  		URI: &apis.URL{
   148  			Scheme:   "https",
   149  			Host:     "tableflip.dev",
   150  			RawQuery: "flip=mattmoor",
   151  		},
   152  	}
   153  	s.Spec.CloudEventOverrides = &CloudEventOverrides{
   154  		Extensions: map[string]string{"boosh": "kakow"},
   155  	}
   156  	s.Status.ObservedGeneration = 42
   157  	s.Status.Conditions = Conditions{{
   158  		// Populate ALL fields
   159  		Type:               SourceConditionSinkProvided,
   160  		Status:             corev1.ConditionTrue,
   161  		LastTransitionTime: apis.VolatileTime{Inner: metav1.NewTime(time.Date(1984, 2, 28, 18, 52, 0, 0, time.UTC))},
   162  	}}
   163  	s.Status.SinkURI = &apis.URL{
   164  		Scheme:   "https",
   165  		Host:     "tableflip.dev",
   166  		RawQuery: "flip=mattmoor",
   167  	}
   168  	s.Status.CloudEventAttributes = []CloudEventAttributes{{
   169  		Type:   "dev.knative.foo",
   170  		Source: "http://knative.dev/knative/eventing",
   171  	}}
   172  }
   173  
   174  // GetListType implements apis.Listable
   175  func (*Source) GetListType() runtime.Object {
   176  	return &SourceList{}
   177  }
   178  
   179  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
   180  
   181  // SourceList is a list of Source resources
   182  type SourceList struct {
   183  	metav1.TypeMeta `json:",inline"`
   184  	metav1.ListMeta `json:"metadata"`
   185  
   186  	Items []Source `json:"items"`
   187  }
   188  
   189  func (s *Source) Validate(ctx context.Context) *apis.FieldError {
   190  	if s == nil {
   191  		return nil
   192  	}
   193  	return s.Spec.Validate(ctx).ViaField("spec")
   194  }
   195  
   196  func (s *SourceSpec) Validate(ctx context.Context) *apis.FieldError {
   197  	if s == nil {
   198  		return apis.ErrMissingField("spec")
   199  	}
   200  	return s.Sink.Validate(ctx).ViaField("sink").
   201  		Also(s.CloudEventOverrides.Validate(ctx).ViaField("ceOverrides"))
   202  }
   203  
   204  func (ceOverrides *CloudEventOverrides) Validate(ctx context.Context) *apis.FieldError {
   205  	if ceOverrides == nil {
   206  		return nil
   207  	}
   208  	for key := range ceOverrides.Extensions {
   209  		if err := validateExtensionName(key); err != nil {
   210  			return err.ViaField("extensions")
   211  		}
   212  	}
   213  	return nil
   214  }
   215  
   216  func validateExtensionName(key string) *apis.FieldError {
   217  	if key == "" {
   218  		return apis.ErrInvalidKeyName(
   219  			key,
   220  			"",
   221  			"keys MUST NOT be empty",
   222  		)
   223  	}
   224  
   225  	for _, c := range key {
   226  		//nolint:staticcheck
   227  		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
   228  			return apis.ErrInvalidKeyName(
   229  				key,
   230  				"",
   231  				"keys are expected to be alphanumeric",
   232  			)
   233  		}
   234  	}
   235  
   236  	return nil
   237  }