Compare commits

...

37 Commits

Author SHA1 Message Date
yangsoon
e320a4b027 add more options for vela-controller (#1769)
* add more option for controller

1. add ConcurrentReconciles for setting the concurrent reconcile number of the controller
2. add DependCheckWait for setting the time to wait for ApplicationConfiguration's dependent-resource ready

* fix test

* add controller reference
2021-06-08 10:30:00 +08:00
wyike
a534c17e8a cp oam pr 329 (#1742)
fix Ci
2021-06-01 18:32:31 +08:00
wyike
7bcf469e8c add long wait as controller args (#1739)
* add long wait as controller args

* fix golint error

* fix ci workflow add git submodles

fix test

* add submodle for Ci
2021-06-01 17:40:15 +08:00
Jianbo Sun
64f07ebd33 Merge pull request #1528 from wonderflow/cp-xxx
update github token
2021-04-19 14:18:09 +08:00
天元
9e0a86212c fix ci 2021-04-19 13:44:59 +08:00
天元
c15a631b9a update github token 2021-04-19 11:22:52 +08:00
Jianbo Sun
df8fb9369b Merge pull request #1185 from wangyikewxgm/cp1172
Cherry pick pr 1172 to release-0.3 branch
2021-03-12 18:58:29 +08:00
wangyike
222906ece0 try to solve failed test 2021-03-12 17:58:51 +08:00
wangyike
ca8aa01c95 cp1172
delete useless test

fix ci error
2021-03-12 17:34:43 +08:00
Jianbo Sun
4cf4fdff30 align workload/trait definition schema with v0.2.2 spec (#1175) 2021-03-11 18:46:02 -08:00
Jianbo Sun
2648f6d100 Merge pull request #1162 from wangyikewxgm/fix-dryrun
fix vela system dryrun command cannot get namespaced definition
2021-03-11 15:25:10 +08:00
Jianbo Sun
3bc014bc8c Merge pull request #1169 from legendtkl/release-0.3
fix arm64 build failed
2021-03-11 10:25:09 +08:00
Weiping Cai
bb9f5402b3 fix arm64 build failed and block our release pipeline
Signed-off-by: Weiping Cai <weiping.cai@daocloud.io>
2021-03-10 22:44:08 +08:00
wangyike
1d3307002c fix dryrun use namespace 2021-03-09 17:18:44 +08:00
Jianbo Sun
37bd952486 Merge pull request #1156 from wangyikewxgm/cp1085
cherry-pick  pr 1085 to release-0.3
2021-03-09 10:55:10 +08:00
wangyike
5c33598fe9 modify controller,webhook,api,chart
solve failed test

add compatibility test for old crd

add app ns for cli

modify compatibility test

solve compatibility problem

add testing for  GetDefinition func with cluster scope CRD

generate code for compatibility-test

move testdata generate to makefile

optimize ci pipeline for compatibility-test
2021-03-09 10:10:48 +08:00
Jianbo Sun
54e806a94c Merge pull request #1152 from yangsoon/fix-workflow
cherry-pick(#1143) delete image after e2e test
2021-03-08 11:48:58 +08:00
yangsoon
740b85c6e1 fix issue #1140 delete image after e2e test 2021-03-08 11:24:42 +08:00
Jianbo Sun
8ceb64d16c Merge pull request #1136 from wonderflow/fixgc
fix garbage collection for apply once force model
2021-03-05 10:51:21 +08:00
天元
5bf11d4142 fix revision enable workload check and prevent GC if replica>0 2021-03-04 18:05:17 +08:00
天元
0229ac775b reduce reconcile too frequency, error will requeue automatically 2021-03-04 16:00:45 +08:00
Jianbo Sun
52e3893bf3 Merge pull request #1114 from wonderflow/fix1
update CRD and fix workload not create first time
2021-03-01 17:58:11 +08:00
天元
419afd27b5 update CRD and fix workload not create first time 2021-03-01 15:05:19 +08:00
Jianbo Sun
52550bb7b5 parent override child when annotation/labels conflicts && one revision will apply once only in force mode && AC.status CRD updated (#1109)
* when annotation/labels passthrough from parent to child conflicts, the parent will override the child.
e.g. 1) AC will override its component/workload; 2) workload will override child-resource; 3) AC will override its trait

* apply once only force will block workload apply when revision not changed even ac generation updated
2021-02-28 22:38:18 -08:00
Jianbo Sun
d6f3f995cd Merge pull request #1111 from captainroy-hy/chp-1041
cherry-pick(1041&1036) fix unit test
2021-02-26 18:57:10 +08:00
roy wang
dc6ab6b8d6 fix unstable unit test
Signed-off-by: roywang <seiwy2010@gmail.com>
2021-02-26 19:18:15 +09:00
roywang
613f1f9b53 quick fix unstable unit test
Signed-off-by: roywang <seiwy2010@gmail.com>
2021-02-26 18:46:10 +09:00
Jianbo Sun
eefc72b0c6 Merge pull request #1099 from captainroy-hy/ut-apply-once
add unit test
2021-02-26 10:26:09 +08:00
Jianbo Sun
1ac66bb3e4 fix component custom revison loop infinitely create revision (#1101) 2021-02-24 23:50:45 -08:00
roywang
ac6d6d1a73 add unit test
Signed-off-by: roywang <seiwy2010@gmail.com>
2021-02-25 02:59:19 +09:00
Jianbo Sun
46f7472bd5 fix apply only once observedGeneration should mark after meet all dependency requirements && add log for apply only once (#1094) 2021-02-24 01:13:27 -08:00
Jianbo Sun
d872430b08 Merge pull request #1095 from wonderflow/cherryp
cherry-pick: remove go-header package dependency
2021-02-23 20:10:30 +08:00
天元
9d2cf16141 remove go-header package dependency 2021-02-23 19:14:57 +08:00
Jianbo Sun
8c9fab1aa1 Merge pull request #1096 from wonderflow/ci
enable CI for release-*
2021-02-23 19:12:44 +08:00
天元
e213779235 enable CI for release-* 2021-02-23 17:53:49 +08:00
Jianbo Sun
e4bf16b618 Merge pull request #1034 from oam-dev/master
merge master to release-0.3 align with v0.3.3
2021-02-07 17:49:09 +08:00
Jianbo Sun
9c64afa1f6 Merge pull request #982 from wonderflow/fix
bump #981 into release-0.3
2021-02-01 14:53:02 +08:00
141 changed files with 4272 additions and 7171 deletions

View File

@@ -7,7 +7,9 @@ on:
- release-*
workflow_dispatch: {}
pull_request:
branches: [master]
branches:
- master
- release-*
env:
# Common versions
@@ -62,11 +64,50 @@ jobs:
flags: unittests
name: codecov-umbrella
compatibility-test:
runs-on: ubuntu-20.04
steps:
- name: Set up Go 1.14
uses: actions/setup-go@v1
with:
go-version: ${{ env.GO_VERSION }}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Cache Go Dependencies
uses: actions/cache@v2
with:
path: .work/pkg
key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-pkg-
- name: Install ginkgo
run: |
sudo apt-get install -y golang-ginkgo-dev
- name: Setup Kind Cluster
uses: engineerd/setup-kind@v0.5.0
with:
version: ${{ env.KIND_VERSION }}
- name: install Kubebuilder
uses: wonderflow/kubebuilder-action@v1.1
- name: Run Make compatibility-test
run: make compatibility-test
- name: Clean up testdata
run: make compatibility-testdata-cleanup
e2e-tests:
runs-on: aliyun
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v2
with:
submodules: true
- name: Setup Go
uses: actions/setup-go@v2
@@ -114,6 +155,10 @@ jobs:
- name: Run e2e tests
run: make e2e-test
- name: Cleanup image
if: ${{ always() }}
run: make image-cleanup
staticcheck:
runs-on: ubuntu-20.04

View File

@@ -37,7 +37,7 @@ jobs:
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login docker.io
uses: docker/login-action@v1
with:

View File

@@ -58,20 +58,6 @@ jobs:
asset_path: ./_bin/vela-linux-amd64.zip
asset_name: vela-${{ steps.get_version.outputs.VERSION }}-linux-amd64.zip
asset_content_type: binary/octet-stream
- name: Upload Linux arm64 tar.gz
uses: actions/upload-release-asset@v1.0.2
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./_bin/vela-linux-arm64.tar.gz
asset_name: vela-${{ steps.get_version.outputs.VERSION }}-linux-arm64.tar.gz
asset_content_type: binary/octet-stream
- name: Upload Linux arm64 zip
uses: actions/upload-release-asset@v1.0.2
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./_bin/vela-linux-arm64.zip
asset_name: vela-${{ steps.get_version.outputs.VERSION }}-linux-arm64.zip
asset_content_type: binary/octet-stream
- name: Upload MacOS tar.gz
uses: actions/upload-release-asset@v1.0.2
with:

View File

@@ -8,7 +8,7 @@ VELA_GITVERSION_VAR := github.com/oam-dev/kubevela/version.GitRevision
LDFLAGS ?= "-X $(VELA_VERSION_VAR)=$(VELA_VERSION) -X $(VELA_GITVERSION_VAR)=$(GIT_COMMIT)"
GOX = go run github.com/mitchellh/gox
TARGETS := darwin/amd64 linux/amd64 windows/amd64 linux/arm64
TARGETS := darwin/amd64 linux/amd64 windows/amd64
DIST_DIRS := find * -type d -exec
TIME_LONG = `date +%Y-%m-%d' '%H:%M:%S`
@@ -125,7 +125,7 @@ docker-push:
docker push ${IMG}
e2e-setup:
bin/vela install --set installCertManager=true --image-pull-policy IfNotPresent --image-repo vela-core-test --image-tag $(GIT_COMMIT)
bin/vela install --set installCertManager=true --image-pull-policy IfNotPresent --image-repo vela-core-test --image-tag $(GIT_COMMIT) --depend-check-wait 10s
ginkgo version
ginkgo -v -r e2e/setup
kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=vela-core,app.kubernetes.io/instance=kubevela -n vela-system --timeout=600s
@@ -144,10 +144,28 @@ e2e-test:
CGO_ENABLED=0 go test -timeout 1h -count=1 -v -tags 'integration' ./test/integration
@$(OK) tests pass
compatibility-test: vet lint staticcheck generate-compatibility-testdata
# Run compatibility test with old crd
COMPATIBILITY_TEST=TRUE go test -race ./pkg/...
@$(OK) compatibility-test pass
generate-compatibility-testdata:
mkdir -p ./test/compatibility-test/testdata
go run ./test/compatibility-test/convert/main.go ./charts/vela-core/crds ./test/compatibility-test/testdata
compatibility-testdata-cleanup:
rm -f ./test/compatibility-test/testdata/*
e2e-cleanup:
# Clean up
rm -rf ~/.vela
image-cleanup:
# Delete Docker image
ifneq ($(shell docker images -q vela-core-test:$(GIT_COMMIT)),)
docker image rm -f vela-core-test:$(GIT_COMMIT)
endif
# load docker image to the kind cluster
kind-load:
docker build -t vela-core-test:$(GIT_COMMIT) .

View File

@@ -24,6 +24,20 @@ import (
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
)
// CUE defines the encapsulation in CUE format
type CUE struct {
// Template defines the abstraction template data of the capability, it will replace the old CUE template in extension field.
// Template is a required field if CUE is defined in Capability Definition.
Template string `json:"template"`
}
// Schematic defines the encapsulation of this capability(workload/trait/scope),
// the encapsulation can be defined in different ways, e.g. CUE/HCL(terraform)/KUBE(K8s Object)/HELM, etc...
type Schematic struct {
CUE *CUE `json:"cue,omitempty"`
// TODO(wonderflow): support HCL(terraform)/KUBE(K8s Object)/HELM here.
}
// A DefinitionReference refers to a CustomResourceDefinition by name.
type DefinitionReference struct {
// Name of the referenced CustomResourceDefinition.
@@ -73,10 +87,9 @@ type WorkloadDefinitionSpec struct {
// +optional
Template string `json:"template,omitempty"`
// TemplateType defines the data format of the template, by default it's CUE format
// Terraform HCL, Helm Chart will also be candidates in the near future.
// Schematic defines the data format and template of the encapsulation of the workload
// +optional
TemplateType string `json:"templateType,omitempty"`
Schematic *Schematic `json:"schematic,omitempty"`
// Extension is used for extension needs by OAM platform builders
// +optional
@@ -101,7 +114,7 @@ type Status struct {
// is used to validate the schema of the workload when it is embedded in an OAM
// Component.
// +kubebuilder:printcolumn:JSONPath=".spec.definitionRef.name",name=DEFINITION-NAME,type=string
// +kubebuilder:resource:scope=Cluster,categories={crossplane,oam}
// +kubebuilder:resource:scope=Namespaced,categories={crossplane,oam}
type WorkloadDefinition struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -150,15 +163,9 @@ type TraitDefinitionSpec struct {
// +optional
ConflictsWith []string `json:"conflictsWith,omitempty"`
// Template defines the abstraction template data of the workload, it will replace the old template in extension field.
// the data format depends on templateType, by default it's CUE
// Schematic defines the data format and template of the encapsulation of the trait
// +optional
Template string `json:"template,omitempty"`
// TemplateType defines the data format of the template, by default it's CUE format
// Terraform HCL, Helm Chart will also be candidates in the near future.
// +optional
TemplateType string `json:"templateType,omitempty"`
Schematic *Schematic `json:"schematic,omitempty"`
// Status defines the custom health policy and status message for trait
// +optional
@@ -177,7 +184,7 @@ type TraitDefinitionSpec struct {
// to validate the schema of the trait when it is embedded in an OAM
// ApplicationConfiguration.
// +kubebuilder:printcolumn:JSONPath=".spec.definitionRef.name",name=DEFINITION-NAME,type=string
// +kubebuilder:resource:scope=Cluster,categories={crossplane,oam}
// +kubebuilder:resource:scope=Namespaced,categories={crossplane,oam}
type TraitDefinition struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -219,7 +226,7 @@ type ScopeDefinitionSpec struct {
// to validate the schema of the scope when it is embedded in an OAM
// ApplicationConfiguration.
// +kubebuilder:printcolumn:JSONPath=".spec.definitionRef.name",name=DEFINITION-NAME,type=string
// +kubebuilder:resource:scope=Cluster,categories={crossplane,oam}
// +kubebuilder:resource:scope=Namespaced,categories={crossplane,oam}
type ScopeDefinition struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -412,6 +419,16 @@ type WorkloadTrait struct {
// Message will allow controller to leave some additional information for this trait
Message string `json:"message,omitempty"`
// AppliedGeneration indicates the generation observed by the appConfig controller.
// The same field is also recorded in the annotations of traits.
// A trait is possible to be deleted from cluster after created.
// This field is useful to track the observed generation of traits after they are
// deleted.
AppliedGeneration int64 `json:"appliedGeneration,omitempty"`
// DependencyUnsatisfied notify does the trait has dependency unsatisfied
DependencyUnsatisfied bool `json:"dependencyUnsatisfied,omitempty"`
}
// A ScopeStatus represents the state of a scope.
@@ -439,12 +456,11 @@ type WorkloadStatus struct {
// ComponentRevisionName of current component
ComponentRevisionName string `json:"componentRevisionName,omitempty"`
// ObservedGeneration indicates the generation observed by the appconfig controller.
// The same field is also recorded in the annotations of workloads.
// A workload is possible to be deleted from cluster after created.
// This field is useful to track the observed generation of workloads after they are
// deleted.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// DependencyUnsatisfied notify does the workload has dependency unsatisfied
DependencyUnsatisfied bool `json:"dependencyUnsatisfied,omitempty"`
// AppliedComponentRevision indicates the applied component revision name of this workload
AppliedComponentRevision string `json:"appliedComponentRevision,omitempty"`
// Reference to a workload created by an ApplicationConfiguration.
Reference runtimev1alpha1.TypedReference `json:"workloadRef,omitempty"`

View File

@@ -488,6 +488,21 @@ func (in *CPUResources) DeepCopy() *CPUResources {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CUE) DeepCopyInto(out *CUE) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CUE.
func (in *CUE) DeepCopy() *CUE {
if in == nil {
return nil
}
out := new(CUE)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ChildResourceKind) DeepCopyInto(out *ChildResourceKind) {
*out = *in
@@ -1556,6 +1571,26 @@ func (in *Revision) DeepCopy() *Revision {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Schematic) DeepCopyInto(out *Schematic) {
*out = *in
if in.CUE != nil {
in, out := &in.CUE, &out.CUE
*out = new(CUE)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Schematic.
func (in *Schematic) DeepCopy() *Schematic {
if in == nil {
return nil
}
out := new(Schematic)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ScopeDefinition) DeepCopyInto(out *ScopeDefinition) {
*out = *in
@@ -1767,6 +1802,11 @@ func (in *TraitDefinitionSpec) DeepCopyInto(out *TraitDefinitionSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Schematic != nil {
in, out := &in.Schematic, &out.Schematic
*out = new(Schematic)
(*in).DeepCopyInto(*out)
}
if in.Status != nil {
in, out := &in.Status, &out.Status
*out = new(Status)
@@ -1925,6 +1965,11 @@ func (in *WorkloadDefinitionSpec) DeepCopyInto(out *WorkloadDefinitionSpec) {
*out = new(Status)
**out = **in
}
if in.Schematic != nil {
in, out := &in.Schematic, &out.Schematic
*out = new(Schematic)
(*in).DeepCopyInto(*out)
}
if in.Extension != nil {
in, out := &in.Extension, &out.Extension
*out = new(runtime.RawExtension)

View File

@@ -12,7 +12,7 @@
//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../hack/boilerplate.go.txt paths=./... crd:trivialVersions=true output:artifacts:config=../legacy/charts/vela-core-legacy/crds
//go:generate go run ../legacy/convert/main.go ../legacy/charts/vela-core-legacy/crds
//go:generate go run ../hack/crd/update.go ../charts/vela-core/crds/standard.oam.dev_podspecworkloads.yaml ../legacy/charts/vela-core-legacy/crds/standard.oam.dev_routes.yaml
//go:generate go run ../hack/crd/update.go ../charts/vela-core/crds/standard.oam.dev_podspecworkloads.yaml
package apis

View File

@@ -1,121 +0,0 @@
/*
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 v1alpha1
import (
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// Protocol defines network protocols supported for things like container ports.
type Protocol string
// TriggerType defines the type of trigger
type TriggerType string
// Autoscaler is the Schema for the autoscalers API
// +kubebuilder:object:root=true
// +kubebuilder:resource:categories={oam}
type Autoscaler struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec AutoscalerSpec `json:"spec"`
Status AutoscalerStatus `json:"status,omitempty"`
}
// SetConditions set condition for CR status
func (as *Autoscaler) SetConditions(c ...v1alpha1.Condition) {
as.Status.SetConditions(c...)
}
// GetCondition get condition from CR status
func (as *Autoscaler) GetCondition(conditionType v1alpha1.ConditionType) v1alpha1.Condition {
return as.Status.GetCondition(conditionType)
}
// GetWorkloadReference get workload reference
func (as *Autoscaler) GetWorkloadReference() v1alpha1.TypedReference {
return as.Spec.WorkloadReference
}
// SetWorkloadReference set workload reference
func (as *Autoscaler) SetWorkloadReference(reference v1alpha1.TypedReference) {
as.Spec.WorkloadReference = reference
}
// Trigger defines the trigger of Autoscaler
type Trigger struct {
// Name is the trigger name, if not set, it will be automatically generated and make it globally unique
Name string `json:"name,omitempty"`
// Type allows value in [cpu/memory/storage/ephemeral-storage、cron、pps、qps/rps、custom]
Type TriggerType `json:"type"`
// Condition set the condition when to trigger scaling
Condition map[string]string `json:"condition"`
}
// AutoscalerSpec defines the desired state of Autoscaler
type AutoscalerSpec struct {
// MinReplicas is the minimal replicas
// +optional
MinReplicas *int32 `json:"minReplicas,omitempty"`
// MinReplicas is the maximal replicas
// +optional
MaxReplicas *int32 `json:"maxReplicas,omitempty"`
// Triggers lists all triggers
Triggers []Trigger `json:"triggers"`
// TargetWorkload specify the workload which is going to be scaled,
// it could be WorkloadReference or the child resource of it
TargetWorkload TargetWorkload `json:"targetWorkload,omitempty"`
// WorkloadReference marks the owner of the workload
WorkloadReference v1alpha1.TypedReference `json:"workloadRef,omitempty"`
}
// TargetWorkload holds the a reference to the scale target Object
type TargetWorkload struct {
Name string `json:"name"`
// +optional
APIVersion string `json:"apiVersion,omitempty"`
// +optional
Kind string `json:"kind,omitempty"`
}
// AutoscalerStatus defines the observed state of Autoscaler
type AutoscalerStatus struct {
v1alpha1.ConditionedStatus `json:",inline"`
}
// +kubebuilder:object:root=true
// AutoscalerList contains a list of Autoscaler
type AutoscalerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Autoscaler `json:"items"`
}
func init() {
SchemeBuilder.Register(&Autoscaler{}, &AutoscalerList{})
}

View File

@@ -1,121 +0,0 @@
/*
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 v1alpha1
import (
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/oam-dev/kubevela/pkg/oam"
)
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// MetricsTraitSpec defines the desired state of MetricsTrait
type MetricsTraitSpec struct {
// An endpoint to be monitored by a ServiceMonitor.
ScrapeService ScapeServiceEndPoint `json:"scrapeService"`
// WorkloadReference to the workload whose metrics needs to be exposed
WorkloadReference runtimev1alpha1.TypedReference `json:"workloadRef,omitempty"`
}
// ScapeServiceEndPoint defines a scrapeable endpoint serving Prometheus metrics.
type ScapeServiceEndPoint struct {
// The format of the metrics data,
// The default and only supported format is "prometheus" for now
Format string `json:"format,omitempty"`
// Number or name of the port to access on the pods targeted by the service.
// The default is discovered automatically from podTemplate, metricTrait will create a service for the workload
TargetPort intstr.IntOrString `json:"port,omitempty"`
// Route service traffic to pods with label keys and values matching this
// The default is discovered automatically from podTemplate.
// If no podTemplate, use the labels specified here, or use the labels of the workload
TargetSelector map[string]string `json:"selector,omitempty"`
// HTTP path to scrape for metrics.
// default is /metrics
// +optional
Path string `json:"path,omitempty"`
// Scheme at which metrics should be scraped
// The default and only supported scheme is "http"
// +optional
Scheme string `json:"scheme,omitempty"`
// The default is true
// +optional
Enabled *bool `json:"enabled,omitempty"`
}
// MetricsTraitStatus defines the observed state of MetricsTrait
type MetricsTraitStatus struct {
runtimev1alpha1.ConditionedStatus `json:",inline"`
// ServiceMonitorName managed by this trait
ServiceMonitorName string `json:"serviceMonitorName,omitempty"`
// Port is the real port monitoring
Port intstr.IntOrString `json:"port,omitempty"`
// SelectorLabels is the real labels selected
SelectorLabels map[string]string `json:"selectorLabels,omitempty"`
}
// +kubebuilder:object:root=true
// MetricsTrait is the Schema for the metricstraits API
// +kubebuilder:resource:categories={oam}
// +kubebuilder:subresource:status
type MetricsTrait struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MetricsTraitSpec `json:"spec"`
Status MetricsTraitStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// MetricsTraitList contains a list of MetricsTrait
type MetricsTraitList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MetricsTrait `json:"items"`
}
func init() {
SchemeBuilder.Register(&MetricsTrait{}, &MetricsTraitList{})
}
var _ oam.Trait = &MetricsTrait{}
// SetConditions for set CR condition
func (tr *MetricsTrait) SetConditions(c ...runtimev1alpha1.Condition) {
tr.Status.SetConditions(c...)
}
// GetCondition for get CR condition
func (tr *MetricsTrait) GetCondition(c runtimev1alpha1.ConditionType) runtimev1alpha1.Condition {
return tr.Status.GetCondition(c)
}
// GetWorkloadReference of this MetricsTrait.
func (tr *MetricsTrait) GetWorkloadReference() runtimev1alpha1.TypedReference {
return tr.Spec.WorkloadReference
}
// SetWorkloadReference of this MetricsTrait.
func (tr *MetricsTrait) SetWorkloadReference(r runtimev1alpha1.TypedReference) {
tr.Spec.WorkloadReference = r
}

View File

@@ -1,165 +0,0 @@
/*
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 v1alpha1
import (
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/oam-dev/kubevela/pkg/oam"
)
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// RouteSpec defines the desired state of Route
type RouteSpec struct {
// WorkloadReference to the workload whose metrics needs to be exposed
WorkloadReference runtimev1alpha1.TypedReference `json:"workloadRef,omitempty"`
// Host is the host of the route
Host string `json:"host"`
// TLS indicate route trait will create SSL secret using cert-manager with specified issuer
// If this is nil, route trait will use a selfsigned issuer
TLS *TLS `json:"tls,omitempty"`
// Rules contain multiple rules of route
Rules []Rule `json:"rules,omitempty"`
// Provider indicate which ingress controller implementation the route trait will use, by default it's nginx-ingress
Provider string `json:"provider,omitempty"`
// IngressClass indicate which ingress class the route trait will use, by default it's nginx
IngressClass string `json:"ingressClass,omitempty"`
}
// Rule defines to route rule
type Rule struct {
// Name will become the suffix of underlying ingress created by this rule, if not, will use index as suffix.
Name string `json:"name,omitempty"`
// Path is location Path, default for "/"
Path string `json:"path,omitempty"`
// RewriteTarget will rewrite request from Path to RewriteTarget path.
RewriteTarget string `json:"rewriteTarget,omitempty"`
// CustomHeaders pass a custom list of headers to the backend service.
CustomHeaders map[string]string `json:"customHeaders,omitempty"`
// DefaultBackend will become the ingress default backend if the backend is not available
DefaultBackend *runtimev1alpha1.TypedReference `json:"defaultBackend,omitempty"`
// Backend indicate how to connect backend service
// If it's nil, will auto discovery
Backend *Backend `json:"backend,omitempty"`
}
// TLS defines certificate issuer and type for mTLS configuration
type TLS struct {
IssuerName string `json:"issuerName,omitempty"`
// Type indicate the issuer is ClusterIssuer or Issuer(namespace issuer), by default, it's Issuer
// +kubebuilder:default:=Issuer
Type IssuerType `json:"type,omitempty"`
}
// IssuerType defines the type of issuer
type IssuerType string
const (
// ClusterIssuer is a cluster level type of issuer
ClusterIssuer IssuerType = "ClusterIssuer"
// NamespaceIssuer is the default one
NamespaceIssuer IssuerType = "Issuer"
)
// Backend defines backend configure for route trait.
// Route will automatically discover podSpec and label for BackendService.
// If BackendService is already set, discovery won't work.
// If BackendService is not set, the discovery mechanism will work.
type Backend struct {
// ReadTimeout used for setting read timeout duration for backend service, the unit is second.
ReadTimeout int `json:"readTimeout,omitempty"`
// SendTimeout used for setting send timeout duration for backend service, the unit is second.
SendTimeout int `json:"sendTimeout,omitempty"`
// BackendService specifies the backend K8s service and port, it's optional
BackendService *BackendServiceRef `json:"backendService,omitempty"`
}
// BackendServiceRef specifies the backend K8s service and port, if specified, the two fields are all required
type BackendServiceRef struct {
// Port allow you direct specify backend service port.
Port intstr.IntOrString `json:"port"`
// ServiceName allow you direct specify K8s service for backend service.
ServiceName string `json:"serviceName"`
}
// RouteStatus defines the observed state of Route
type RouteStatus struct {
Ingresses []runtimev1alpha1.TypedReference `json:"ingresses,omitempty"`
Service *runtimev1alpha1.TypedReference `json:"service,omitempty"`
Status string `json:"status,omitempty"`
runtimev1alpha1.ConditionedStatus `json:",inline"`
}
// Route is the Schema for the routes API
// +kubebuilder:object:root=true
// +kubebuilder:resource:categories={oam}
// +kubebuilder:subresource:status
type Route struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec RouteSpec `json:"spec,omitempty"`
Status RouteStatus `json:"status,omitempty"`
}
// RouteList contains a list of Route
// +kubebuilder:object:root=true
type RouteList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Route `json:"items"`
}
func init() {
SchemeBuilder.Register(&Route{}, &RouteList{})
}
var _ oam.Trait = &Route{}
// SetConditions set condition for CR status
func (r *Route) SetConditions(c ...runtimev1alpha1.Condition) {
r.Status.SetConditions(c...)
}
// GetCondition get condition from CR status
func (r *Route) GetCondition(c runtimev1alpha1.ConditionType) runtimev1alpha1.Condition {
return r.Status.GetCondition(c)
}
// GetWorkloadReference of this Route Trait.
func (r *Route) GetWorkloadReference() runtimev1alpha1.TypedReference {
return r.Spec.WorkloadReference
}
// SetWorkloadReference of this Route Trait.
func (r *Route) SetWorkloadReference(rt runtimev1alpha1.TypedReference) {
r.Spec.WorkloadReference = rt
}

View File

@@ -26,151 +26,6 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Autoscaler) DeepCopyInto(out *Autoscaler) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Autoscaler.
func (in *Autoscaler) DeepCopy() *Autoscaler {
if in == nil {
return nil
}
out := new(Autoscaler)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Autoscaler) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AutoscalerList) DeepCopyInto(out *AutoscalerList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Autoscaler, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalerList.
func (in *AutoscalerList) DeepCopy() *AutoscalerList {
if in == nil {
return nil
}
out := new(AutoscalerList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AutoscalerList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AutoscalerSpec) DeepCopyInto(out *AutoscalerSpec) {
*out = *in
if in.MinReplicas != nil {
in, out := &in.MinReplicas, &out.MinReplicas
*out = new(int32)
**out = **in
}
if in.MaxReplicas != nil {
in, out := &in.MaxReplicas, &out.MaxReplicas
*out = new(int32)
**out = **in
}
if in.Triggers != nil {
in, out := &in.Triggers, &out.Triggers
*out = make([]Trigger, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
out.TargetWorkload = in.TargetWorkload
out.WorkloadReference = in.WorkloadReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalerSpec.
func (in *AutoscalerSpec) DeepCopy() *AutoscalerSpec {
if in == nil {
return nil
}
out := new(AutoscalerSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AutoscalerStatus) DeepCopyInto(out *AutoscalerStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalerStatus.
func (in *AutoscalerStatus) DeepCopy() *AutoscalerStatus {
if in == nil {
return nil
}
out := new(AutoscalerStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Backend) DeepCopyInto(out *Backend) {
*out = *in
if in.BackendService != nil {
in, out := &in.BackendService, &out.BackendService
*out = new(BackendServiceRef)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backend.
func (in *Backend) DeepCopy() *Backend {
if in == nil {
return nil
}
out := new(Backend)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackendServiceRef) DeepCopyInto(out *BackendServiceRef) {
*out = *in
out.Port = in.Port
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendServiceRef.
func (in *BackendServiceRef) DeepCopy() *BackendServiceRef {
if in == nil {
return nil
}
out := new(BackendServiceRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CanaryMetric) DeepCopyInto(out *CanaryMetric) {
*out = *in
@@ -221,106 +76,6 @@ func (in *MetricsExpectedRange) DeepCopy() *MetricsExpectedRange {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MetricsTrait) DeepCopyInto(out *MetricsTrait) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsTrait.
func (in *MetricsTrait) DeepCopy() *MetricsTrait {
if in == nil {
return nil
}
out := new(MetricsTrait)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MetricsTrait) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MetricsTraitList) DeepCopyInto(out *MetricsTraitList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]MetricsTrait, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsTraitList.
func (in *MetricsTraitList) DeepCopy() *MetricsTraitList {
if in == nil {
return nil
}
out := new(MetricsTraitList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MetricsTraitList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MetricsTraitSpec) DeepCopyInto(out *MetricsTraitSpec) {
*out = *in
in.ScrapeService.DeepCopyInto(&out.ScrapeService)
out.WorkloadReference = in.WorkloadReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsTraitSpec.
func (in *MetricsTraitSpec) DeepCopy() *MetricsTraitSpec {
if in == nil {
return nil
}
out := new(MetricsTraitSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MetricsTraitStatus) DeepCopyInto(out *MetricsTraitStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
out.Port = in.Port
if in.SelectorLabels != nil {
in, out := &in.SelectorLabels, &out.SelectorLabels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsTraitStatus.
func (in *MetricsTraitStatus) DeepCopy() *MetricsTraitStatus {
if in == nil {
return nil
}
out := new(MetricsTraitStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PodSpecWorkload) DeepCopyInto(out *PodSpecWorkload) {
*out = *in
@@ -677,228 +432,3 @@ func (in *RolloutWebhookPayload) DeepCopy() *RolloutWebhookPayload {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Route) DeepCopyInto(out *Route) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Route.
func (in *Route) DeepCopy() *Route {
if in == nil {
return nil
}
out := new(Route)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Route) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouteList) DeepCopyInto(out *RouteList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Route, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteList.
func (in *RouteList) DeepCopy() *RouteList {
if in == nil {
return nil
}
out := new(RouteList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *RouteList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouteSpec) DeepCopyInto(out *RouteSpec) {
*out = *in
out.WorkloadReference = in.WorkloadReference
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(TLS)
**out = **in
}
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]Rule, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteSpec.
func (in *RouteSpec) DeepCopy() *RouteSpec {
if in == nil {
return nil
}
out := new(RouteSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouteStatus) DeepCopyInto(out *RouteStatus) {
*out = *in
if in.Ingresses != nil {
in, out := &in.Ingresses, &out.Ingresses
*out = make([]corev1alpha1.TypedReference, len(*in))
copy(*out, *in)
}
if in.Service != nil {
in, out := &in.Service, &out.Service
*out = new(corev1alpha1.TypedReference)
**out = **in
}
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteStatus.
func (in *RouteStatus) DeepCopy() *RouteStatus {
if in == nil {
return nil
}
out := new(RouteStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Rule) DeepCopyInto(out *Rule) {
*out = *in
if in.CustomHeaders != nil {
in, out := &in.CustomHeaders, &out.CustomHeaders
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.DefaultBackend != nil {
in, out := &in.DefaultBackend, &out.DefaultBackend
*out = new(corev1alpha1.TypedReference)
**out = **in
}
if in.Backend != nil {
in, out := &in.Backend, &out.Backend
*out = new(Backend)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rule.
func (in *Rule) DeepCopy() *Rule {
if in == nil {
return nil
}
out := new(Rule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ScapeServiceEndPoint) DeepCopyInto(out *ScapeServiceEndPoint) {
*out = *in
out.TargetPort = in.TargetPort
if in.TargetSelector != nil {
in, out := &in.TargetSelector, &out.TargetSelector
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Enabled != nil {
in, out := &in.Enabled, &out.Enabled
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScapeServiceEndPoint.
func (in *ScapeServiceEndPoint) DeepCopy() *ScapeServiceEndPoint {
if in == nil {
return nil
}
out := new(ScapeServiceEndPoint)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLS) DeepCopyInto(out *TLS) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS.
func (in *TLS) DeepCopy() *TLS {
if in == nil {
return nil
}
out := new(TLS)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TargetWorkload) DeepCopyInto(out *TargetWorkload) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetWorkload.
func (in *TargetWorkload) DeepCopy() *TargetWorkload {
if in == nil {
return nil
}
out := new(TargetWorkload)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Trigger) DeepCopyInto(out *Trigger) {
*out = *in
if in.Condition != nil {
in, out := &in.Condition, &out.Condition
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Trigger.
func (in *Trigger) DeepCopy() *Trigger {
if in == nil {
return nil
}
out := new(Trigger)
in.DeepCopyInto(out)
return out
}

View File

@@ -380,16 +380,18 @@ spec:
items:
description: A WorkloadStatus represents the status of a workload.
properties:
appliedComponentRevision:
description: AppliedComponentRevision indicates the applied component revision name of this workload
type: string
componentName:
description: ComponentName that produced this workload.
type: string
componentRevisionName:
description: ComponentRevisionName of current component
type: string
observedGeneration:
description: ObservedGeneration indicates the generation observed by the appconfig controller. The same field is also recorded in the annotations of workloads. A workload is possible to be deleted from cluster after created. This field is useful to track the observed generation of workloads after they are deleted.
format: int64
type: integer
dependencyUnsatisfied:
description: DependencyUnsatisfied notify does the workload has dependency unsatisfied
type: boolean
scopes:
description: Scopes associated with this workload.
items:
@@ -430,6 +432,13 @@ spec:
items:
description: A WorkloadTrait represents a trait associated with a workload and its status
properties:
appliedGeneration:
description: AppliedGeneration indicates the generation observed by the appConfig controller. The same field is also recorded in the annotations of traits. A trait is possible to be deleted from cluster after created. This field is useful to track the observed generation of traits after they are deleted.
format: int64
type: integer
dependencyUnsatisfied:
description: DependencyUnsatisfied notify does the trait has dependency unsatisfied
type: boolean
message:
description: Message will allow controller to leave some additional information for this trait
type: string

View File

@@ -17,7 +17,7 @@ spec:
listKind: ScopeDefinitionList
plural: scopedefinitions
singular: scopedefinition
scope: Cluster
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.definitionRef.name

View File

@@ -17,7 +17,7 @@ spec:
listKind: TraitDefinitionList
plural: traitdefinitions
singular: traitdefinition
scope: Cluster
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.definitionRef.name
@@ -68,6 +68,19 @@ spec:
revisionEnabled:
description: Revision indicates whether a trait is aware of component revision
type: boolean
schematic:
description: Schematic defines the data format and template of the encapsulation of the trait
properties:
cue:
description: CUE defines the encapsulation in CUE format
properties:
template:
description: Template defines the abstraction template data of the capability, it will replace the old CUE template in extension field. Template is a required field if CUE is defined in Capability Definition.
type: string
required:
- template
type: object
type: object
status:
description: Status defines the custom health policy and status message for trait
properties:
@@ -78,12 +91,6 @@ spec:
description: HealthPolicy defines the health check policy for the abstraction
type: string
type: object
template:
description: Template defines the abstraction template data of the workload, it will replace the old template in extension field. the data format depends on templateType, by default it's CUE
type: string
templateType:
description: TemplateType defines the data format of the template, by default it's CUE format Terraform HCL, Helm Chart will also be candidates in the near future.
type: string
workloadRefPath:
description: WorkloadRefPath indicates where/if a trait accepts a workloadRef object
type: string

View File

@@ -17,7 +17,7 @@ spec:
listKind: WorkloadDefinitionList
plural: workloaddefinitions
singular: workloaddefinition
scope: Cluster
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.definitionRef.name
@@ -82,6 +82,19 @@ spec:
revisionLabel:
description: RevisionLabel indicates which label for underlying resources(e.g. pods) of this workload can be used by trait to create resource selectors(e.g. label selector for pods).
type: string
schematic:
description: Schematic defines the data format and template of the encapsulation of the workload
properties:
cue:
description: CUE defines the encapsulation in CUE format
properties:
template:
description: Template defines the abstraction template data of the capability, it will replace the old CUE template in extension field. Template is a required field if CUE is defined in Capability Definition.
type: string
required:
- template
type: object
type: object
status:
description: Status defines the custom health policy and status message for workload
properties:
@@ -95,9 +108,6 @@ spec:
template:
description: Template defines the abstraction template data of the workload, it will replace the old template in extension field. the data format depends on templateType, by default it's CUE
type: string
templateType:
description: TemplateType defines the data format of the template, by default it's CUE format Terraform HCL, Helm Chart will also be candidates in the near future.
type: string
required:
- definitionRef
type: object

View File

@@ -1,143 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.2.4
creationTimestamp: null
name: autoscalers.standard.oam.dev
spec:
group: standard.oam.dev
names:
categories:
- oam
kind: Autoscaler
listKind: AutoscalerList
plural: autoscalers
singular: autoscaler
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Autoscaler is the Schema for the autoscalers API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: AutoscalerSpec defines the desired state of Autoscaler
properties:
maxReplicas:
description: MinReplicas is the maximal replicas
format: int32
type: integer
minReplicas:
description: MinReplicas is the minimal replicas
format: int32
type: integer
targetWorkload:
description: TargetWorkload specify the workload which is going to be scaled, it could be WorkloadReference or the child resource of it
properties:
apiVersion:
type: string
kind:
type: string
name:
type: string
required:
- name
type: object
triggers:
description: Triggers lists all triggers
items:
description: Trigger defines the trigger of Autoscaler
properties:
condition:
additionalProperties:
type: string
description: Condition set the condition when to trigger scaling
type: object
name:
description: Name is the trigger name, if not set, it will be automatically generated and make it globally unique
type: string
type:
description: Type allows value in [cpu/memory/storage/ephemeral-storage、cron、pps、qps/rps、custom]
type: string
required:
- condition
- type
type: object
type: array
workloadRef:
description: WorkloadReference marks the owner of the workload
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
required:
- triggers
type: object
status:
description: AutoscalerStatus defines the observed state of Autoscaler
properties:
conditions:
description: Conditions of the resource.
items:
description: A Condition that may apply to a resource.
properties:
lastTransitionTime:
description: LastTransitionTime is the last time this condition transitioned from one status to another.
format: date-time
type: string
message:
description: A Message containing details about this condition's last transition from one status to another, if any.
type: string
reason:
description: A Reason for this condition's last transition from one status to another.
type: string
status:
description: Status of this condition; is it currently True, False, or Unknown?
type: string
type:
description: Type of this condition. At most one of each condition type may apply to a resource at any point in time.
type: string
required:
- lastTransitionTime
- reason
- status
- type
type: object
type: array
type: object
required:
- spec
type: object
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,145 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.2.4
creationTimestamp: null
name: metricstraits.standard.oam.dev
spec:
group: standard.oam.dev
names:
categories:
- oam
kind: MetricsTrait
listKind: MetricsTraitList
plural: metricstraits
singular: metricstrait
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: MetricsTrait is the Schema for the metricstraits API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: MetricsTraitSpec defines the desired state of MetricsTrait
properties:
scrapeService:
description: An endpoint to be monitored by a ServiceMonitor.
properties:
enabled:
description: The default is true
type: boolean
format:
description: The format of the metrics data, The default and only supported format is "prometheus" for now
type: string
path:
description: HTTP path to scrape for metrics. default is /metrics
type: string
port:
anyOf:
- type: integer
- type: string
description: Number or name of the port to access on the pods targeted by the service. The default is discovered automatically from podTemplate, metricTrait will create a service for the workload
x-kubernetes-int-or-string: true
scheme:
description: Scheme at which metrics should be scraped The default and only supported scheme is "http"
type: string
selector:
additionalProperties:
type: string
description: Route service traffic to pods with label keys and values matching this The default is discovered automatically from podTemplate. If no podTemplate, use the labels specified here, or use the labels of the workload
type: object
type: object
workloadRef:
description: WorkloadReference to the workload whose metrics needs to be exposed
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
required:
- scrapeService
type: object
status:
description: MetricsTraitStatus defines the observed state of MetricsTrait
properties:
conditions:
description: Conditions of the resource.
items:
description: A Condition that may apply to a resource.
properties:
lastTransitionTime:
description: LastTransitionTime is the last time this condition transitioned from one status to another.
format: date-time
type: string
message:
description: A Message containing details about this condition's last transition from one status to another, if any.
type: string
reason:
description: A Reason for this condition's last transition from one status to another.
type: string
status:
description: Status of this condition; is it currently True, False, or Unknown?
type: string
type:
description: Type of this condition. At most one of each condition type may apply to a resource at any point in time.
type: string
required:
- lastTransitionTime
- reason
- status
- type
type: object
type: array
port:
anyOf:
- type: integer
- type: string
description: Port is the real port monitoring
x-kubernetes-int-or-string: true
selectorLabels:
additionalProperties:
type: string
description: SelectorLabels is the real labels selected
type: object
serviceMonitorName:
description: ServiceMonitorName managed by this trait
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,232 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.2.4
creationTimestamp: null
name: routes.standard.oam.dev
spec:
group: standard.oam.dev
names:
categories:
- oam
kind: Route
listKind: RouteList
plural: routes
singular: route
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Route is the Schema for the routes API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: RouteSpec defines the desired state of Route
properties:
host:
description: Host is the host of the route
type: string
ingressClass:
description: IngressClass indicate which ingress class the route trait will use, by default it's nginx
type: string
provider:
description: Provider indicate which ingress controller implementation the route trait will use, by default it's nginx-ingress
type: string
rules:
description: Rules contain multiple rules of route
items:
description: Rule defines to route rule
properties:
backend:
description: Backend indicate how to connect backend service If it's nil, will auto discovery
properties:
backendService:
description: BackendService specifies the backend K8s service and port, it's optional
properties:
port:
anyOf:
- type: integer
- type: string
description: Port allow you direct specify backend service port.
x-kubernetes-int-or-string: true
serviceName:
description: ServiceName allow you direct specify K8s service for backend service.
type: string
required:
- port
- serviceName
type: object
readTimeout:
description: ReadTimeout used for setting read timeout duration for backend service, the unit is second.
type: integer
sendTimeout:
description: SendTimeout used for setting send timeout duration for backend service, the unit is second.
type: integer
type: object
customHeaders:
additionalProperties:
type: string
description: CustomHeaders pass a custom list of headers to the backend service.
type: object
defaultBackend:
description: DefaultBackend will become the ingress default backend if the backend is not available
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
name:
description: Name will become the suffix of underlying ingress created by this rule, if not, will use index as suffix.
type: string
path:
description: Path is location Path, default for "/"
type: string
rewriteTarget:
description: RewriteTarget will rewrite request from Path to RewriteTarget path.
type: string
type: object
type: array
tls:
description: TLS indicate route trait will create SSL secret using cert-manager with specified issuer If this is nil, route trait will use a selfsigned issuer
properties:
issuerName:
type: string
type:
default: Issuer
description: Type indicate the issuer is ClusterIssuer or Issuer(namespace issuer), by default, it's Issuer
type: string
type: object
workloadRef:
description: WorkloadReference to the workload whose metrics needs to be exposed
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
required:
- host
type: object
status:
description: RouteStatus defines the observed state of Route
properties:
conditions:
description: Conditions of the resource.
items:
description: A Condition that may apply to a resource.
properties:
lastTransitionTime:
description: LastTransitionTime is the last time this condition transitioned from one status to another.
format: date-time
type: string
message:
description: A Message containing details about this condition's last transition from one status to another, if any.
type: string
reason:
description: A Reason for this condition's last transition from one status to another.
type: string
status:
description: Status of this condition; is it currently True, False, or Unknown?
type: string
type:
description: Type of this condition. At most one of each condition type may apply to a resource at any point in time.
type: string
required:
- lastTransitionTime
- reason
- status
- type
type: object
type: array
ingresses:
items:
description: A TypedReference refers to an object by Name, Kind, and APIVersion. It is commonly used to reference cluster-scoped objects or objects where the namespace is already known.
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
type: array
service:
description: A TypedReference refers to an object by Name, Kind, and APIVersion. It is commonly used to reference cluster-scoped objects or objects where the namespace is already known.
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
status:
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -2,6 +2,7 @@ apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: containerizedworkloads.core.oam.dev
namespace: {{.Values.systemDefinitionNamespace}}
spec:
definitionRef:
name: containerizedworkloads.core.oam.dev

View File

@@ -2,7 +2,7 @@ apiVersion: core.oam.dev/v1alpha2
kind: ScopeDefinition
metadata:
name: healthscopes.core.oam.dev
namespace: default
namespace: {{.Values.systemDefinitionNamespace}}
spec:
workloadRefsPath: spec.workloadRefs
allowComponentOverlap: true

View File

@@ -6,6 +6,7 @@ metadata:
definition.oam.dev/description: "Configures K8s ingress and service to enable web traffic for your service.
Please use route trait in cap center for advanced usage."
name: ingress
namespace: {{.Values.systemDefinitionNamespace}}
spec:
status:
customStatus: |-
@@ -20,50 +21,52 @@ spec:
appliesToWorkloads:
- webservice
- worker
template: |
parameter: {
domain: string
http: [string]: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata:
name: context.name
spec: {
selector:
"app.oam.dev/component": context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
},
]
}
}
outputs: ingress: {
apiVersion: "networking.k8s.io/v1beta1"
kind: "Ingress"
metadata:
name: context.name
spec: {
rules: [{
host: parameter.domain
http: {
paths: [
for k, v in parameter.http {
path: k
backend: {
serviceName: context.name
servicePort: v
}
},
]
}
}]
}
}
schematic:
cue:
template: |
parameter: {
domain: string
http: [string]: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata:
name: context.name
spec: {
selector:
"app.oam.dev/component": context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
},
]
}
}
outputs: ingress: {
apiVersion: "networking.k8s.io/v1beta1"
kind: "Ingress"
metadata:
name: context.name
spec: {
rules: [{
host: parameter.domain
http: {
paths: [
for k, v in parameter.http {
path: k
backend: {
serviceName: context.name
servicePort: v
}
},
]
}
}]
}
}

View File

@@ -5,6 +5,7 @@ metadata:
annotations:
definition.oam.dev/description: "Configures replicas for your service."
name: scaler
namespace: {{.Values.systemDefinitionNamespace}}
spec:
appliesToWorkloads:
- webservice
@@ -12,17 +13,19 @@ spec:
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
template: |
output: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
//+usage=Replicas of the workload
replicas: *1 | int
}
schematic:
cue:
template: |
output: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
//+usage=Replicas of the workload
replicas: *1 | int
}

View File

@@ -3,44 +3,47 @@ apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: task
namespace: {{.Values.systemDefinitionNamespace}}
annotations:
definition.oam.dev/description: "Describes jobs that run code or a script to completion."
spec:
definitionRef:
name: jobs.batch
template: |
output: {
apiVersion: "batch/v1"
kind: "Job"
spec: {
parallelism: parameter.count
completions: parameter.count
template: spec: {
restartPolicy: parameter.restart
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
parameter: {
// +usage=specify number of tasks to run in parallel
// +short=c
count: *1 | int
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Define the job restart policy, the value can only be Never or OnFailure. By default, it's Never.
restart: *"Never" | string
// +usage=Commands to run in the container
cmd?: [...string]
}
schematic:
cue:
template: |
output: {
apiVersion: "batch/v1"
kind: "Job"
spec: {
parallelism: parameter.count
completions: parameter.count
template: spec: {
restartPolicy: parameter.restart
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
parameter: {
// +usage=specify number of tasks to run in parallel
// +short=c
count: *1 | int
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Define the job restart policy, the value can only be Never or OnFailure. By default, it's Never.
restart: *"Never" | string
// +usage=Commands to run in the container
cmd?: [...string]
}

View File

@@ -3,89 +3,92 @@ apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: webservice
namespace: {{.Values.systemDefinitionNamespace}}
annotations:
definition.oam.dev/description: "Describes long-running, scalable, containerized services that have a stable network endpoint to receive external network traffic from customers.
If workload type is skipped for any service defined in Appfile, it will be defaulted to `webservice` type."
spec:
definitionRef:
name: deployments.apps
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
if parameter["env"] != _|_ {
env: parameter.env
}
if context["config"] != _|_ {
env: context.config
}
ports: [{
containerPort: parameter.port
}]
if parameter["cpu"] != _|_ {
resources: {
limits:
cpu: parameter.cpu
requests:
cpu: parameter.cpu
}
}
}]
}
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
// +usage=Which port do you want customer traffic sent to
// +short=p
port: *80 | int
// +usage=Define arguments by using environment variables
env?: [...{
// +usage=Environment variable name
name: string
// +usage=The value of the environment variable
value?: string
// +usage=Specifies a source the value of this var should come from
valueFrom?: {
// +usage=Selects a key of a secret in the pod's namespace
secretKeyRef: {
// +usage=The name of the secret in the pod's namespace to select from
name: string
// +usage=The key of the secret to select from. Must be a valid secret key
key: string
}
}
}]
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
cpu?: string
}
schematic:
cue:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
if parameter["env"] != _|_ {
env: parameter.env
}
if context["config"] != _|_ {
env: context.config
}
ports: [{
containerPort: parameter.port
}]
if parameter["cpu"] != _|_ {
resources: {
limits:
cpu: parameter.cpu
requests:
cpu: parameter.cpu
}
}
}]
}
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
// +usage=Which port do you want customer traffic sent to
// +short=p
port: *80 | int
// +usage=Define arguments by using environment variables
env?: [...{
// +usage=Environment variable name
name: string
// +usage=The value of the environment variable
value?: string
// +usage=Specifies a source the value of this var should come from
valueFrom?: {
// +usage=Selects a key of a secret in the pod's namespace
secretKeyRef: {
// +usage=The name of the secret in the pod's namespace to select from
name: string
// +usage=The key of the secret to select from. Must be a valid secret key
key: string
}
}
}]
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
cpu?: string
}

View File

@@ -3,44 +3,47 @@ apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: worker
namespace: {{.Values.systemDefinitionNamespace}}
annotations:
definition.oam.dev/description: "Describes long-running, scalable, containerized services that running at backend. They do NOT have network endpoint to receive external network traffic."
spec:
definitionRef:
name: deployments.apps
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
}
schematic:
cue:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
}

View File

@@ -117,6 +117,8 @@ spec:
{{ if ne .Values.disableCaps "" }}
- "--disable-caps={{ .Values.disableCaps }}"
{{ end }}
- "--concurrent-reconciles={{ .Values.concurrentReconciles }}"
- "--depend-check-wait={{ .Values.dependCheckWait }}"
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: {{ quote .Values.image.pullPolicy }}
resources:

View File

@@ -84,3 +84,11 @@ certificate:
secretName: webhook-server-cert
mountPath: /etc/k8s-webhook-certs
caBundle: replace-me
systemDefinitionNamespace: vela-system
# concurrentReconciles is the concurrent reconcile number of the controller
concurrentReconciles: 4
# dependCheckWait is the time to wait for ApplicationConfiguration's dependent-resource ready
dependCheckWait: 30s

View File

@@ -13,7 +13,6 @@ import (
"syscall"
"time"
monitoring "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1"
"github.com/crossplane/crossplane-runtime/pkg/logging"
injectorv1alpha1 "github.com/oam-dev/trait-injector/api/v1alpha1"
injectorcontroller "github.com/oam-dev/trait-injector/controllers"
@@ -21,7 +20,6 @@ import (
"github.com/oam-dev/trait-injector/pkg/plugin"
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1"
kedav1alpha1 "github.com/wonderflow/keda-api/api/v1alpha1"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -38,6 +36,7 @@ import (
oamcontroller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
oamv1alpha2 "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils/system"
oamwebhook "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev"
velawebhook "github.com/oam-dev/kubevela/pkg/webhook/standard.oam.dev"
@@ -59,11 +58,9 @@ func init() {
_ = clientgoscheme.AddToScheme(scheme)
_ = crdv1.AddToScheme(scheme)
_ = oamcore.AddToScheme(scheme)
_ = monitoring.AddToScheme(scheme)
_ = velacore.AddToScheme(scheme)
_ = injectorv1alpha1.AddToScheme(scheme)
_ = certmanager.AddToScheme(scheme)
_ = kedav1alpha1.AddToScheme(scheme)
_ = kruise.AddToScheme(scheme)
// +kubebuilder:scaffold:scheme
}
@@ -103,8 +100,13 @@ func main() {
"custom-revision-hook-url is a webhook url which will let KubeVela core to call with applicationConfiguration and component info and return a customized component revision")
flag.StringVar(&disableCaps, "disable-caps", "", "To be disabled builtin capability list.")
flag.StringVar(&storageDriver, "storage-driver", driver.LocalDriverName, "Application file save to the storage driver")
flag.DurationVar(&syncPeriod, "informer-re-sync-interval", 5*time.Minute,
"controller shared informer lister full re-sync period")
flag.DurationVar(&syncPeriod, "informer-re-sync-interval", 2*time.Hour, "controller shared informer lister full re-sync period. The default value is 2 hours")
flag.StringVar(&oam.SystemDefinitonNamespace, "system-definition-namespace", "vela-system", "define the namespace of the system-level definition")
flag.DurationVar(&controllerArgs.LongWait, "long-wait", 1*time.Minute, "long-wait is controller next reconcile interval time like 30s, 2m etc. The default value is 1m,"+
" you can set it to 0 for no reconcile routine after success")
flag.IntVar(&controllerArgs.ConcurrentReconciles, "concurrent-reconciles", 4, "concurrent-reconciles is the concurrent reconcile number of the controller. The default value is 4")
flag.DurationVar(&controllerArgs.DependCheckWait, "depend-check-wait", 30*time.Second, "depend-check-wait is the time to wait for ApplicationConfiguration's dependent-resource ready."+
"The default value is 30s, which means if dependent resources were not prepared, the ApplicationConfiguration would be reconciled after 30s.")
flag.Parse()
// setup logging
@@ -126,8 +128,12 @@ func main() {
setupLog.Info(fmt.Sprintf("KubeVela Version: %s, GIT Revision: %s.", version.VelaVersion, version.GitRevision))
setupLog.Info(fmt.Sprintf("Disable Capabilities: %s.", disableCaps))
setupLog.Info(fmt.Sprintf("core init with definition namespace %s", oam.SystemDefinitonNamespace))
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
restConfig := ctrl.GetConfigOrDie()
restConfig.UserAgent = kubevelaName + "/" + version.GitRevision
mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
LeaderElection: enableLeaderElection,

View File

@@ -12,57 +12,60 @@ spec:
isHealth: (context.output.status.readyReplicas > 0) && (context.output.status.readyReplicas == context.output.status.replicas)
customStatus: |-
message: "type: " + context.output.spec.template.spec.containers[0].image + ",\t enemies:" + context.outputs.gameconfig.data.enemies
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
schematic:
cue:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
envFrom: [{
configMapRef: name: context.name + "game-config"
}]
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
spec: {
containers: [{
name: context.name
image: parameter.image
envFrom: [{
configMapRef: name: context.name + "game-config"
}]
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
outputs: gameconfig: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: context.name + "game-config"
}
data: {
enemies: parameter.enemies
lives: parameter.lives
}
}
outputs: gameconfig: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: context.name + "game-config"
}
data: {
enemies: parameter.enemies
lives: parameter.lives
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
lives: string
enemies: string
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
lives: string
enemies: string
}
---
apiVersion: core.oam.dev/v1alpha2
@@ -75,45 +78,47 @@ spec:
message: "type: "+ context.outputs.service.spec.type +",\t clusterIP:"+ context.outputs.service.spec.clusterIP+",\t ports:"+ "\(context.outputs.service.spec.ports[0].port)"+",\t domain"+context.outputs.ingress.spec.rules[0].host
healthPolicy: |
isHealth: len(context.outputs.service.spec.clusterIP) > 0
template: |
parameter: {
domain: string
http: [string]: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
spec: {
selector:
app: context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
},
]
}
}
outputs: ingress: {
apiVersion: "networking.k8s.io/v1beta1"
kind: "Ingress"
metadata:
name: context.name
spec: {
rules: [{
host: parameter.domain
http: {
paths: [
for k, v in parameter.http {
path: k
backend: {
serviceName: context.name
servicePort: v
}
},
]
}
}]
}
}
schematic:
cue:
template: |
parameter: {
domain: string
http: [string]: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
spec: {
selector:
app: context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
},
]
}
}
outputs: ingress: {
apiVersion: "networking.k8s.io/v1beta1"
kind: "Ingress"
metadata:
name: context.name
spec: {
rules: [{
host: parameter.domain
http: {
paths: [
for k, v in parameter.http {
path: k
backend: {
serviceName: context.name
servicePort: v
}
},
]
}
}]
}
}

View File

@@ -0,0 +1,24 @@
# KubeVela Controller Parameters Reference
| parameter | type | default | describe |
| :-------------------------: | :----: | :-------------------------------: | :----------------------------------------------------------: |
| use-webhook | bool | false | Enable Admission Webhook |
| use-trait-injector | bool | false | Enable TraitInjector |
| webhook-cert-dir | string | /k8s-webhook-server/serving-certs | Admission webhook cert/key dir. |
| metrics-addr | string | :8080 | The address the metric endpoint binds to. |
| enable-leader-election | bool | false | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. |
| leader-election-namespace | string | "" | Determines the namespace in which the leader election configmap will be created. |
| log-file-path | string | "" | The file to write logs to. |
| log-retain-date | int | 7 | The number of days of logs history to retain. |
| log-compress | bool | true | Enable compression on the rotated logs. |
| revision-limit | int | 50 | revision-limit is the maximum number of revisions that will be maintained. The default value is 50. |
| health-addr | string | :9440 | The address the health endpoint binds to. |
| apply-once-only | string | false | For the purpose of some production environment that workload or trait should not be affected if no spec change, available options: on, off, force. |
| custom-revision-hook-url | string | "" | custom-revision-hook-url is a webhook url which will let KubeVela core to call with applicationConfiguration and component info and return a customized component revision |
| disable-caps | string | "" | To be disabled builtin capability list. |
| storage-driver | string | Local | Application file save to the storage driver |
| informer-re-sync-interval | time | 2h | controller shared informer lister full re-sync period |
| system-definition-namespace | string | vela-system | define the namespace of the system-level definition |
| long-wait | time | 1m | long-wait is controller next reconcile interval time like 30s, 2m etc. The default value is 1m, you can set it to 0 for no reconcile routine after success |
| concurrent-reconciles | int | 4 | concurrent-reconciles is the concurrent reconcile number of the controller. |
| depend-check-wait | time | 30s | depend-check-wait is the time to wait for ApplicationConfiguration's dependent-resource ready. |

View File

@@ -130,8 +130,13 @@ The same mechanism also works for Trait as well as Workload.
### Apply Once Only Force
Based on the same mechanism as `apply-once-only`, `apply-once-only-force` allows to skip re-creating a workload or trait that has already been DELETED from the cluster if its spec is not changed.
It's regarded as a stronger case of `apply-once-only`.
Based on the same mechanism as `apply-once-only`, `apply-once-only-force` has a more strict method for apply only once.
It allows to skip re-creating a workload or trait that has already been DELETED from the cluster if its spec is not changed.
Besides the condition in `apply-once-only`, `apply-once-only-force` has one more condition:
- if the component revision not changed, the workload will not be applied.
## Usage

110
docs/en/application.md Normal file
View File

@@ -0,0 +1,110 @@
# Designing Application
Application encapsulation and abstraction is achieved by the `Application` custom resource.
## Example
The sample application below claimed a `backend` component with *Worker* workload type, and a `frontend` component with *Web Service* workload type.
Moreover, the `frontend` component claimed `sidecar` and `autoscaler` traits which means the workload will be automatically injected with a `fluentd` sidecar and scale from 1-100 replicas triggered by CPU usage.
> For detailed definition about `Application` *workload type* and *traits*, please read the [core concepts](/en/concepts.md#application) documentation.
```yaml
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: website
spec:
components:
- name: backend
type: worker
settings:
image: busybox
cmd:
- sleep
- '1000'
- name: frontend
type: webservice
settings:
image: nginx
traits:
- name: autoscaler
properties:
min: 1
max: 10
cpuPercent: 60
- name: sidecar
properties:
name: "sidecar-test"
image: "fluentd"
```
The `type: worker` means the specification of this workload (claimed in following `settings` section) will be enforced by a `WorkloadDefinition` object named `worker` as below:
```yaml
apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: worker
annotations:
definition.oam.dev/description: "Describes long-running, scalable, containerized services that running at backend. They do NOT have network endpoint to receive external network traffic."
spec:
schematic:
cue:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
parameter: {
image: string
cmd?: [...string]
}
```
Hence, the `settings` section of `backend` only supports two parameters: `image` and `cmd`, this is enforced by the `parameter` list of the `.spec.template` field of the definition.
The similar extensible abstraction mechanism also applies to traits. For example, `name: autoscaler` in `frontend` means its trait specification (i.e. `properties` section) will be enforced by a `TraitDefinition` object named `autoscaler` as below:
> TBD: a autoscaler TraitDefinition (HPA)
All the definition objects are expected to be defined and installed by platform team. The end users will only focus on `Application` resource (either render it by tools or author it manually).
## Conventions and "Standard Contract"
After the `Application` resource is applied to Kubernetes cluster, the KubeVela runtime will generate and manage the underlying resources instances following below "standard contract" and conventions.
| Label | Description |
| :--: | :---------: |
|`workload.oam.dev/type=<workload definition name>` | The name of its corresponding `WorkloadDefinition` |
|`trait.oam.dev/type=<trait definition name>` | The name of its corresponding `TraitDefinition` |
|`app.oam.dev/name=<app name>` | The name of the application it belongs to |
|`app.oam.dev/component=<component name>` | The name of the component it belongs to |
|`trait.oam.dev/resource=<name of trait resource instance>` | The name of trait resource instance |
> TBD: the revision names and labels for resource instances are currently work in progress.
> TBD: a demo for kubectl apply above Application CR and show full detailed underlying resources.

383
docs/en/cue/basic.md Normal file
View File

@@ -0,0 +1,383 @@
# CUE Basic
This document will explain how to use [CUE](https://cuelang.org/) as templating module in KubeVela. Please make sure you have already learned about `Application` custom resource and how it leverage templating modules for application encapsulation and abstraction.
## Why CUE?
The reasons for KubeVela supports CUE as first class templating solution can be concluded as below:
- **CUE is designed for large scale configuration.** CUE has the ability to understand a
configuration worked on by engineers across a whole company and to safely change a value that modifies thousands of objects in a configuration. This aligns very well with KubeVela's original goal to define and ship production level applications at web scale.
- **CUE supports first-class code generation and automation.** CUE can integrate with existing tools and workflows naturally while other tools would have to build complex custom solutions. For example, generate OpenAPI schemas wigh Go code. This is how KubeVela build developer tools and GUI interfaces based on the CUE templates.
- **CUE integrates very well with Go.**
KubeVela is built with GO just like most projects in Kubernetes system. CUE is also implemented in and exposes a rich API in Go. KubeVela integrates with CUE as its core library and works as a Kubernetes controller. With the help of CUE, KubeVela can easily handle data constraint problems.
> Pleas also check [The Configuration Complexity Curse](https://blog.cedriccharly.com/post/20191109-the-configuration-complexity-curse/) and [The Logic of CUE](https://cuelang.org/docs/concepts/logic/) for more details.
## Parameter and Template
A very simple `WorkloadDefinition` is like below:
```yaml
apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: mydeploy
spec:
definitionRef:
name: deployments.apps
schematic:
cue:
template: |
parameter: {
name: string
image: string
}
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": parameter.name
}
template: {
metadata: labels: {
"app.oam.dev/component": parameter.name
}
spec: {
containers: [{
name: parameter.name
image: parameter.image
}]
}
}
}
}
```
The `template` field in this definition is a CUE module, it defines two keywords for KubeVela to build the application abstraction:
- The `parameter` defines the input parameters from end user, i.e. the configurable fields in the abstraction.
- The `output` defines the template for the abstraction.
## CUE Template Step by Step
Let's say as the platform team, we only want to allow end user configure `image` and `name` fields in the `Application` abstraction, and automatically generate all rest of the fields. How can we use CUE to achieve this?
We can start from the final resource we envision the platform will generate based on user inputs, for example:
```yaml
apiVersion: apps/v1
kind: Deployment
meadata:
name: mytest # user inputs
spec:
template:
spec:
containers:
- name: mytest # user inputs
env:
- name: a
value: b
image: nginx:v1 # user inputs
metadata:
labels:
app.oam.dev/component: mytest # generate by user inputs
selector:
matchLabels:
app.oam.dev/component: mytest # generate by user inputs
```
Then we can just convert this YAML to JSON and put the whole JSON object into the `output` keyword field:
```cue
output: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: "mytest"
spec: {
selector: matchLabels: {
"app.oam.dev/component": "mytest"
}
template: {
metadata: labels: {
"app.oam.dev/component": "mytest"
}
spec: {
containers: [{
name: "mytest"
image: "nginx:v1"
env: [{name:"a",value:"b"}]
}]
}
}
}
}
```
Since CUE as a superset of JSON, we can use:
* C style comments,
* quotes may be omitted from field names without special characters,
* commas at the end of fields are optional,
* comma after last element in list is allowed,
* outer curly braces are optional.
After that, we can then add `parameter` keyword, and use it as a variable reference, this is the very basic CUE feature for templating.
```cue
parameter: {
name: string
image: string
}
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": parameter.name
}
template: {
metadata: labels: {
"app.oam.dev/component": parameter.name
}
spec: {
containers: [{
name: parameter.name
image: parameter.image
}]
}
}
}
}
```
Finally, you can put the above CUE module in the `template` field of `WorkloadDefinition` object and give it a name. Then end users can now author `Application` resource reference this definition as workload type and only have `name` and `image` as configurable parameters.
## Advanced CUE Templating
In this section, we will introduce advanced CUE templating features supports in KubeVela.
### Structural Parameter
This is the most commonly used feature. It enables us to expose complex data structure for end users. For example, environment variable list.
A simple guide is as below:
1. Define a type in the CUE template, it includes a struct (`other`), a string and an integer.
```
#Config: {
name: string
value: int
other: {
key: string
value: string
}
}
```
2. In the `parameter` section, reference above type and define it as `[...#Config]`. Then it can accept inputs from end users as an array list.
```
parameter: {
name: string
image: string
configSingle: #Config
config: [...#Config] # array list parameter
}
```
3. In the `output` section, simply do templating as other parameters.
```
output: {
...
spec: {
containers: [{
name: parameter.name
image: parameter.image
env: parameter.config
}]
}
...
}
```
4. As long as you install a workload definition object (e.g. `mydeploy`) with above template in the system, a new field `config` will be available to use like below:
```yaml
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: website
spec:
components:
- name: backend
type: mydeploy
settings:
image: crccheck/hello-world
name: mysvc
config: # a complex parameter
- name: a
value: 1
other:
key: mykey
value: myvalue
```
### Conditional Parameter
Conditional parameter can be used to do `if..else` logic in template.
Below is an example that when `useENV=true`, it will render env section, otherwise, it will not.
```
parameter: {
name: string
image: string
useENV: bool
}
output: {
...
spec: {
containers: [{
name: parameter.name
image: parameter.image
if parameter.useENV == true {
env: [{name: "my-env", value: "my-value"}]
}
}]
}
...
}
```
### Optional and Default Value
Optional parameter can be skipped, that usually works together with conditional logic.
Specifically, if some field does not exit, the CUE grammar is `if _variable_ != _|_`, the example is like below:
```
parameter: {
name: string
image: string
config?: [...#Config]
}
output: {
...
spec: {
containers: [{
name: parameter.name
image: parameter.image
if parameter.config != _|_ {
config: parameter.config
}
}]
}
...
}
```
Default Value is marked with a `*` prefix. It's used like
```
parameter: {
name: string
image: *"nginx:v1" | string
port: *80 | int
number: *123.4 | float
}
output: {
...
spec: {
containers: [{
name: parameter.name
image: parameter.image
}]
}
...
}
```
So if a parameter field is neither a parameter with default value nor a conditional field, it's a required value.
### Loop
#### Loop for Map
```cue
parameter: {
name: string
image: string
env: [string]: string
}
output: {
spec: {
containers: [{
name: parameter.name
image: parameter.image
env: [
for k, v in parameter.env {
name: k
value: v
},
]
}]
}
}
```
#### Loop for Slice
```cue
parameter: {
name: string
image: string
env: [...{name:string,value:string}]
}
output: {
...
spec: {
containers: [{
name: parameter.name
image: parameter.image
env: [
for _, v in parameter.env {
name: v.name
value: v.value
},
]
}]
}
}
```
### Import CUE Internal Packages
CUE has many [internal packages](https://pkg.go.dev/cuelang.org/go@v0.2.2/pkg) which also can be used in KubeVela.
Below is an example that use `strings.Join` to `concat` string list to one string.
```cue
import ("strings")
parameter: {
outputs: [{ip: "1.1.1.1", hostname: "xxx.com"}, {ip: "2.2.2.2", hostname: "yyy.com"}]
}
output: {
spec: {
if len(parameter.outputs) > 0 {
_x: [ for _, v in parameter.outputs {
"\(v.ip) \(v.hostname)"
}]
message: "Visiting URL: " + strings.Join(_x, "")
}
}
}
```
## Summary
Overall, CUE is a very powerful templating language which could help platform team create extensible application encapsulation and abstraction with ease.

View File

@@ -0,0 +1,221 @@
# Defining Workload Types
In this section, we will introduce more examples of using CUE to define workload types.
## Basic Usage
The very basic usage of CUE in workload is to extend a Kubernetes resource as a workload type(via `WorkloadDefinition`) and expose configurable parameters to users.
A Deployment as workload type:
```yaml
apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: worker
spec:
definitionRef:
name: deployments.apps
schematic:
cue:
template: |
parameter: {
name: string
image: string
}
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": parameter.name
}
template: {
metadata: labels: {
"app.oam.dev/component": parameter.name
}
spec: {
containers: [{
name: parameter.name
image: parameter.image
}]
}}}
}
```
A Job as workload type:
```yaml
apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: task
annotations:
definition.oam.dev/description: "Describes jobs that run code or a script to completion."
spec:
definitionRef:
name: jobs.batch
schematic:
cue:
template: |
output: {
apiVersion: "batch/v1"
kind: "Job"
spec: {
parallelism: parameter.count
completions: parameter.count
template: spec: {
restartPolicy: parameter.restart
containers: [{
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
parameter: {
count: *1 | int
image: string
restart: *"Never" | string
cmd?: [...string]
}
```
## Context
When you want to reference the runtime instance name for an app, you can use the `conext` keyword to define `parameter`.
KubeVela runtime provides a `context` struct including app name(`context.appName`) and component name(`context.name`).
```cue
context: {
appName: string
name: string
}
```
Values of the context will be automatically generated before the underlying resources are applied.
This is why you can reference the context variable as value in the template.
```yaml
parameter: {
image: string
}
output: {
...
spec: {
containers: [{
name: context.name
image: parameter.image
}]
}
...
}
```
## Composition
A workload type can contain multiple Kubernetes resources, for example, we can define a `webserver` workload type that is composed by Deployment and Service.
Note that in this case, you MUST define the template of component instance in `output` section, and leave all the other templates in `outputs` with resource name claimed. The format MUST be `outputs:<unique-name>:<full template>`.
> This is how KubeVela know which resource is the running instance of the application component.
Below is the example:
```yaml
apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: webserver
annotations:
definition.oam.dev/description: "webserver is a combo of Deployment + Service"
spec:
definitionRef:
name: deployments.apps
schematic:
cue:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
if parameter["env"] != _|_ {
env: parameter.env
}
if context["config"] != _|_ {
env: context.config
}
ports: [{
containerPort: parameter.port
}]
if parameter["cpu"] != _|_ {
resources: {
limits:
cpu: parameter.cpu
requests:
cpu: parameter.cpu
}
}
}]
}
}
}
}
// an extra template
outputs: service: {
apiVersion: "v1"
kind: "Service"
spec: {
selector: {
"app.oam.dev/component": context.name
}
ports: [
{
port: parameter.port
targetPort: parameter.port
},
]
}
}
parameter: {
image: string
cmd?: [...string]
port: *80 | int
env?: [...{
name: string
value?: string
valueFrom?: {
secretKeyRef: {
name: string
key: string
}
}
}]
cpu?: string
}
```
> TBD: a generated resource example for above workload definition.

View File

@@ -1,5 +1,23 @@
# Automatically scale workloads by resource utilization metrics and cron
## Prerequisite
Make sure auto-scaler trait controller is installed in your cluster
Install auto-scaler trait controller with helm
1. Add helm chart repo for autoscaler trait
```shell script
helm repo add oam.catalog http://oam.dev/catalog/
```
2. Update the chart repo
```shell script
helm repo update
```
3. Install autoscaler trait controller
```shell script
helm install --create-namespace -n vela-system autoscalertrait oam.catalog/autoscalertrait
> Note: autoscale is one of the extension capabilities [installed from cap center](../cap-center.md),
> please install it if you can't find it in `vela traits`.

View File

@@ -1,7 +1,27 @@
# Monitoring Application
## Prerequisite
Make sure metrics trait controller is installed in your cluster
Install metrics trait controller with helm
1. Add helm chart repo for metrics trait
```shell script
helm repo add oam.catalog http://oam.dev/catalog/
```
2. Update the chart repo
```shell script
helm repo update
```
3. Install metrics trait controller
```shell script
helm install --create-namespace -n vela-system metricstrait oam.catalog/metricstrait
> Note: metrics is one of the extension capabilities [installed from cap center](../cap-center.md),
> please install it if you can't find it in `vela traits`.
## Setting metrics policy
If your application has exposed metrics, you can easily tell the platform how to collect the metrics data from your app with `metrics` capability.

View File

@@ -1,11 +1,28 @@
# Setting Routes
The `route` section is used to configure the access to your app.
## Prerequisite
Make sure route trait controller is installed in your cluster
The `route` section is used to configure the access to your app.
Install route trait controller with helm
1. Add helm chart repo for route trait
```shell script
helm repo add oam.catalog http://oam.dev/catalog/
```
2. Update the chart repo
```shell script
helm repo update
```
3. Install route trait controller
```shell script
helm install --create-namespace -n vela-system routetrait oam.catalog/routetrait
> Note: route is one of the extension capabilities [installed from cap center](../cap-center.md),
> please install it if you can't find it in `vela traits`.
The `route` section is used to configure the access to your app.
Add routing config under `express-server`:
```yaml

View File

@@ -335,30 +335,32 @@ kind: TraitDefinition
metadata:
name: auth-service
spec:
template: |
parameter: {
serviceURL: string
}
processing: {
output: {
token?: string
}
# task shall output a json result and output will correlate fields by name.
http: {
method: *"GET" | string
url: parameter.serviceURL
request: {
body ?: bytes
header: {}
trailer: {}
schematic:
cue:
template: |
parameter: {
serviceURL: string
}
}
}
patch: {
data: token: processing.output.token
}
processing: {
output: {
token?: string
}
// task shall output a json result and output will correlate fields by name.
http: {
method: *"GET" | string
url: parameter.serviceURL
request: {
body?: bytes
header: {}
trailer: {}
}
}
}
patch: {
data: token: processing.output.token
}
```

3
go.mod
View File

@@ -29,6 +29,7 @@ require (
github.com/google/go-github/v32 v32.1.0
github.com/gosuri/uitable v0.0.4
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174
github.com/klauspost/compress v1.10.5 // indirect
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mholt/archiver/v3 v3.3.0
@@ -38,7 +39,6 @@ require (
github.com/onsi/ginkgo v1.13.0
github.com/onsi/gomega v1.10.3
github.com/openkruise/kruise-api v0.7.0
github.com/openservicemesh/osm v0.3.0
github.com/pkg/errors v0.9.1
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
github.com/spf13/cobra v1.1.1
@@ -50,7 +50,6 @@ require (
github.com/ugorji/go v1.2.1 // indirect
github.com/wercker/stern v0.0.0-20190705090245-4fa46dd6987f
github.com/wonderflow/cert-manager-api v1.0.3
github.com/wonderflow/keda-api v0.0.0-20201026084048-e7c39fa208e8
go.uber.org/zap v1.15.0
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 // indirect
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect

1059
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -8,14 +8,12 @@ import (
"path/filepath"
"github.com/oam-dev/kubevela/hack/utils"
"github.com/openservicemesh/osm/pkg/cli"
)
func main() {
// Path relative to the Makefile where this is invoked.
chartPath := filepath.Join("charts", "vela-core")
source, err := cli.GetChartSource(chartPath)
source, err := utils.GetChartSource(chartPath)
if err != nil {
fmt.Fprintln(os.Stderr, "error getting chart source:", err)
os.Exit(1)

View File

@@ -2,7 +2,16 @@ package utils
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
helm "helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
)
// FprintZipData converts zip binary contents to a string literal.
@@ -28,3 +37,36 @@ func FprintZipData(dest *bytes.Buffer, zipData []byte) {
fmt.Fprintf(dest, "\\x%02x", b)
}
}
// GetChartSource is a helper to convert a filepath to a chart to a
// base64-encoded, gzipped tarball.
func GetChartSource(path string) (string, error) {
pack := helm.NewPackage()
packagedPath, err := pack.Run(path, nil)
if err != nil {
return "", err
}
defer os.Remove(packagedPath)
packaged, err := ioutil.ReadFile(packagedPath)
if err != nil {
return "", err
}
b64Encoded := bytes.NewBuffer(nil)
enc := base64.NewEncoder(base64.StdEncoding, b64Encoded)
_, err = io.Copy(enc, bytes.NewReader(packaged))
if err != nil {
return "", err
}
return b64Encoded.String(), nil
}
// LoadChart is a helper to turn a base64-encoded, gzipped tarball into a chart.
func LoadChart(source string) (*chart.Chart, error) {
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(source))
tgz := bytes.NewBuffer(nil)
_, err := io.Copy(tgz, dec)
if err != nil {
return nil, err
}
return loader.LoadArchive(tgz)
}

View File

@@ -5,6 +5,7 @@ metadata:
definition.oam.dev/description: "Configures K8s ingress and service to enable web traffic for your service.
Please use route trait in cap center for advanced usage."
name: ingress
namespace: {{.Values.systemDefinitionNamespace}}
spec:
status:
customStatus: |-
@@ -19,4 +20,6 @@ spec:
appliesToWorkloads:
- webservice
- worker
template: |
schematic:
cue:
template: |

View File

@@ -4,6 +4,7 @@ metadata:
annotations:
definition.oam.dev/description: "Configures replicas for your service."
name: scaler
namespace: {{.Values.systemDefinitionNamespace}}
spec:
appliesToWorkloads:
- webservice
@@ -11,4 +12,6 @@ spec:
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
template: |
schematic:
cue:
template: |

View File

@@ -2,9 +2,12 @@ apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: task
namespace: {{.Values.systemDefinitionNamespace}}
annotations:
definition.oam.dev/description: "Describes jobs that run code or a script to completion."
spec:
definitionRef:
name: jobs.batch
template: |
schematic:
cue:
template: |

View File

@@ -2,10 +2,13 @@ apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: webservice
namespace: {{.Values.systemDefinitionNamespace}}
annotations:
definition.oam.dev/description: "Describes long-running, scalable, containerized services that have a stable network endpoint to receive external network traffic from customers.
If workload type is skipped for any service defined in Appfile, it will be defaulted to `webservice` type."
spec:
definitionRef:
name: deployments.apps
template: |
schematic:
cue:
template: |

View File

@@ -2,9 +2,12 @@ apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: worker
namespace: {{.Values.systemDefinitionNamespace}}
annotations:
definition.oam.dev/description: "Describes long-running, scalable, containerized services that running at backend. They do NOT have network endpoint to receive external network traffic."
spec:
definitionRef:
name: deployments.apps
template: |
schematic:
cue:
template: |

View File

@@ -16,7 +16,7 @@ echo "# Code generated by KubeVela templates. DO NOT EDIT." >> tmpC
for filename in `ls cue`; do
cat "cue/${filename}" > tmp
echo "" >> tmp
sed -i.bak 's/^/ /' tmp
sed -i.bak 's/^/ /' tmp
nameonly="${filename%.*}"

View File

@@ -380,16 +380,18 @@ spec:
items:
description: A WorkloadStatus represents the status of a workload.
properties:
appliedComponentRevision:
description: AppliedComponentRevision indicates the applied component revision name of this workload
type: string
componentName:
description: ComponentName that produced this workload.
type: string
componentRevisionName:
description: ComponentRevisionName of current component
type: string
observedGeneration:
description: ObservedGeneration indicates the generation observed by the appconfig controller. The same field is also recorded in the annotations of workloads. A workload is possible to be deleted from cluster after created. This field is useful to track the observed generation of workloads after they are deleted.
format: int64
type: integer
dependencyUnsatisfied:
description: DependencyUnsatisfied notify does the workload has dependency unsatisfied
type: boolean
scopes:
description: Scopes associated with this workload.
items:
@@ -430,6 +432,13 @@ spec:
items:
description: A WorkloadTrait represents a trait associated with a workload and its status
properties:
appliedGeneration:
description: AppliedGeneration indicates the generation observed by the appConfig controller. The same field is also recorded in the annotations of traits. A trait is possible to be deleted from cluster after created. This field is useful to track the observed generation of traits after they are deleted.
format: int64
type: integer
dependencyUnsatisfied:
description: DependencyUnsatisfied notify does the trait has dependency unsatisfied
type: boolean
message:
description: Message will allow controller to leave some additional information for this trait
type: string

View File

@@ -21,7 +21,7 @@ spec:
listKind: ScopeDefinitionList
plural: scopedefinitions
singular: scopedefinition
scope: Cluster
scope: Namespaced
subresources: {}
validation:
openAPIV3Schema:

View File

@@ -21,7 +21,7 @@ spec:
listKind: TraitDefinitionList
plural: traitdefinitions
singular: traitdefinition
scope: Cluster
scope: Namespaced
subresources: {}
validation:
openAPIV3Schema:
@@ -67,6 +67,19 @@ spec:
revisionEnabled:
description: Revision indicates whether a trait is aware of component revision
type: boolean
schematic:
description: Schematic defines the data format and template of the encapsulation of the trait
properties:
cue:
description: CUE defines the encapsulation in CUE format
properties:
template:
description: Template defines the abstraction template data of the capability, it will replace the old CUE template in extension field. Template is a required field if CUE is defined in Capability Definition.
type: string
required:
- template
type: object
type: object
status:
description: Status defines the custom health policy and status message for trait
properties:
@@ -77,12 +90,6 @@ spec:
description: HealthPolicy defines the health check policy for the abstraction
type: string
type: object
template:
description: Template defines the abstraction template data of the workload, it will replace the old template in extension field. the data format depends on templateType, by default it's CUE
type: string
templateType:
description: TemplateType defines the data format of the template, by default it's CUE format Terraform HCL, Helm Chart will also be candidates in the near future.
type: string
workloadRefPath:
description: WorkloadRefPath indicates where/if a trait accepts a workloadRef object
type: string

View File

@@ -21,7 +21,7 @@ spec:
listKind: WorkloadDefinitionList
plural: workloaddefinitions
singular: workloaddefinition
scope: Cluster
scope: Namespaced
subresources: {}
validation:
openAPIV3Schema:
@@ -81,6 +81,19 @@ spec:
revisionLabel:
description: RevisionLabel indicates which label for underlying resources(e.g. pods) of this workload can be used by trait to create resource selectors(e.g. label selector for pods).
type: string
schematic:
description: Schematic defines the data format and template of the encapsulation of the workload
properties:
cue:
description: CUE defines the encapsulation in CUE format
properties:
template:
description: Template defines the abstraction template data of the capability, it will replace the old CUE template in extension field. Template is a required field if CUE is defined in Capability Definition.
type: string
required:
- template
type: object
type: object
status:
description: Status defines the custom health policy and status message for workload
properties:
@@ -94,9 +107,6 @@ spec:
template:
description: Template defines the abstraction template data of the workload, it will replace the old template in extension field. the data format depends on templateType, by default it's CUE
type: string
templateType:
description: TemplateType defines the data format of the template, by default it's CUE format Terraform HCL, Helm Chart will also be candidates in the near future.
type: string
required:
- definitionRef
type: object

View File

@@ -1,144 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.2.4
creationTimestamp: null
name: autoscalers.standard.oam.dev
spec:
group: standard.oam.dev
names:
categories:
- oam
kind: Autoscaler
listKind: AutoscalerList
plural: autoscalers
singular: autoscaler
scope: Namespaced
validation:
openAPIV3Schema:
description: Autoscaler is the Schema for the autoscalers API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: AutoscalerSpec defines the desired state of Autoscaler
properties:
maxReplicas:
description: MinReplicas is the maximal replicas
format: int32
type: integer
minReplicas:
description: MinReplicas is the minimal replicas
format: int32
type: integer
targetWorkload:
description: TargetWorkload specify the workload which is going to be scaled, it could be WorkloadReference or the child resource of it
properties:
apiVersion:
type: string
kind:
type: string
name:
type: string
required:
- name
type: object
triggers:
description: Triggers lists all triggers
items:
description: Trigger defines the trigger of Autoscaler
properties:
condition:
additionalProperties:
type: string
description: Condition set the condition when to trigger scaling
type: object
name:
description: Name is the trigger name, if not set, it will be automatically generated and make it globally unique
type: string
type:
description: Type allows value in [cpu/memory/storage/ephemeral-storage、cron、pps、qps/rps、custom]
type: string
required:
- condition
- type
type: object
type: array
workloadRef:
description: WorkloadReference marks the owner of the workload
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
required:
- triggers
type: object
status:
description: AutoscalerStatus defines the observed state of Autoscaler
properties:
conditions:
description: Conditions of the resource.
items:
description: A Condition that may apply to a resource.
properties:
lastTransitionTime:
description: LastTransitionTime is the last time this condition transitioned from one status to another.
format: date-time
type: string
message:
description: A Message containing details about this condition's last transition from one status to another, if any.
type: string
reason:
description: A Reason for this condition's last transition from one status to another.
type: string
status:
description: Status of this condition; is it currently True, False, or Unknown?
type: string
type:
description: Type of this condition. At most one of each condition type may apply to a resource at any point in time.
type: string
required:
- lastTransitionTime
- reason
- status
- type
type: object
type: array
type: object
required:
- spec
type: object
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,146 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.2.4
creationTimestamp: null
name: metricstraits.standard.oam.dev
spec:
group: standard.oam.dev
names:
categories:
- oam
kind: MetricsTrait
listKind: MetricsTraitList
plural: metricstraits
singular: metricstrait
scope: Namespaced
subresources:
status: {}
validation:
openAPIV3Schema:
description: MetricsTrait is the Schema for the metricstraits API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: MetricsTraitSpec defines the desired state of MetricsTrait
properties:
scrapeService:
description: An endpoint to be monitored by a ServiceMonitor.
properties:
enabled:
description: The default is true
type: boolean
format:
description: The format of the metrics data, The default and only supported format is "prometheus" for now
type: string
path:
description: HTTP path to scrape for metrics. default is /metrics
type: string
port:
anyOf:
- type: integer
- type: string
description: Number or name of the port to access on the pods targeted by the service. The default is discovered automatically from podTemplate, metricTrait will create a service for the workload
x-kubernetes-int-or-string: true
scheme:
description: Scheme at which metrics should be scraped The default and only supported scheme is "http"
type: string
selector:
additionalProperties:
type: string
description: Route service traffic to pods with label keys and values matching this The default is discovered automatically from podTemplate. If no podTemplate, use the labels specified here, or use the labels of the workload
type: object
type: object
workloadRef:
description: WorkloadReference to the workload whose metrics needs to be exposed
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
required:
- scrapeService
type: object
status:
description: MetricsTraitStatus defines the observed state of MetricsTrait
properties:
conditions:
description: Conditions of the resource.
items:
description: A Condition that may apply to a resource.
properties:
lastTransitionTime:
description: LastTransitionTime is the last time this condition transitioned from one status to another.
format: date-time
type: string
message:
description: A Message containing details about this condition's last transition from one status to another, if any.
type: string
reason:
description: A Reason for this condition's last transition from one status to another.
type: string
status:
description: Status of this condition; is it currently True, False, or Unknown?
type: string
type:
description: Type of this condition. At most one of each condition type may apply to a resource at any point in time.
type: string
required:
- lastTransitionTime
- reason
- status
- type
type: object
type: array
port:
anyOf:
- type: integer
- type: string
description: Port is the real port monitoring
x-kubernetes-int-or-string: true
selectorLabels:
additionalProperties:
type: string
description: SelectorLabels is the real labels selected
type: object
serviceMonitorName:
description: ServiceMonitorName managed by this trait
type: string
type: object
required:
- spec
type: object
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,232 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.2.4
creationTimestamp: null
name: routes.standard.oam.dev
spec:
group: standard.oam.dev
names:
categories:
- oam
kind: Route
listKind: RouteList
plural: routes
singular: route
scope: Namespaced
subresources:
status: {}
validation:
openAPIV3Schema:
description: Route is the Schema for the routes API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: RouteSpec defines the desired state of Route
properties:
host:
description: Host is the host of the route
type: string
ingressClass:
description: IngressClass indicate which ingress class the route trait will use, by default it's nginx
type: string
provider:
description: Provider indicate which ingress controller implementation the route trait will use, by default it's nginx-ingress
type: string
rules:
description: Rules contain multiple rules of route
items:
description: Rule defines to route rule
properties:
backend:
description: Backend indicate how to connect backend service If it's nil, will auto discovery
properties:
backendService:
description: BackendService specifies the backend K8s service and port, it's optional
properties:
port:
anyOf:
- type: integer
- type: string
description: Port allow you direct specify backend service port.
x-kubernetes-int-or-string: true
serviceName:
description: ServiceName allow you direct specify K8s service for backend service.
type: string
required:
- port
- serviceName
type: object
readTimeout:
description: ReadTimeout used for setting read timeout duration for backend service, the unit is second.
type: integer
sendTimeout:
description: SendTimeout used for setting send timeout duration for backend service, the unit is second.
type: integer
type: object
customHeaders:
additionalProperties:
type: string
description: CustomHeaders pass a custom list of headers to the backend service.
type: object
defaultBackend:
description: DefaultBackend will become the ingress default backend if the backend is not available
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
name:
description: Name will become the suffix of underlying ingress created by this rule, if not, will use index as suffix.
type: string
path:
description: Path is location Path, default for "/"
type: string
rewriteTarget:
description: RewriteTarget will rewrite request from Path to RewriteTarget path.
type: string
type: object
type: array
tls:
description: TLS indicate route trait will create SSL secret using cert-manager with specified issuer If this is nil, route trait will use a selfsigned issuer
properties:
issuerName:
type: string
type:
description: Type indicate the issuer is ClusterIssuer or Issuer(namespace issuer), by default, it's Issuer
type: string
type: object
workloadRef:
description: WorkloadReference to the workload whose metrics needs to be exposed
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
required:
- host
type: object
status:
description: RouteStatus defines the observed state of Route
properties:
conditions:
description: Conditions of the resource.
items:
description: A Condition that may apply to a resource.
properties:
lastTransitionTime:
description: LastTransitionTime is the last time this condition transitioned from one status to another.
format: date-time
type: string
message:
description: A Message containing details about this condition's last transition from one status to another, if any.
type: string
reason:
description: A Reason for this condition's last transition from one status to another.
type: string
status:
description: Status of this condition; is it currently True, False, or Unknown?
type: string
type:
description: Type of this condition. At most one of each condition type may apply to a resource at any point in time.
type: string
required:
- lastTransitionTime
- reason
- status
- type
type: object
type: array
ingresses:
items:
description: A TypedReference refers to an object by Name, Kind, and APIVersion. It is commonly used to reference cluster-scoped objects or objects where the namespace is already known.
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
type: array
service:
description: A TypedReference refers to an object by Name, Kind, and APIVersion. It is commonly used to reference cluster-scoped objects or objects where the namespace is already known.
properties:
apiVersion:
description: APIVersion of the referenced object.
type: string
kind:
description: Kind of the referenced object.
type: string
name:
description: Name of the referenced object.
type: string
uid:
description: UID of the referenced object.
type: string
required:
- apiVersion
- kind
- name
type: object
status:
type: string
type: object
type: object
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -34,7 +34,8 @@ func ApplyTerraform(app *v1alpha2.Application, k8sClient client.Client, ioStream
var nativeVelaComponents []v1alpha2.ApplicationComponent
// parse template
appParser := NewApplicationParser(k8sClient, dm)
appFile, err := appParser.GenerateAppFile(app.Name, app)
// TODO(wangyike) this context only for compiling success, lately mabey surport setting sysNs and appNs in api-server or cli
appFile, err := appParser.GenerateAppFile(context.TODO(), app.Name, app)
if err != nil {
return nil, fmt.Errorf("failed to parse appfile: %w", err)
}

View File

@@ -1,6 +1,8 @@
package appfile
import (
"context"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/pkg/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
@@ -130,12 +132,12 @@ func NewApplicationParser(cli client.Client, dm discoverymapper.DiscoveryMapper)
}
// GenerateAppFile converts an application to an Appfile
func (p *Parser) GenerateAppFile(name string, app *v1alpha2.Application) (*Appfile, error) {
func (p *Parser) GenerateAppFile(ctx context.Context, name string, app *v1alpha2.Application) (*Appfile, error) {
appfile := new(Appfile)
appfile.Name = name
var wds []*Workload
for _, comp := range app.Spec.Components {
wd, err := p.parseWorkload(comp)
wd, err := p.parseWorkload(ctx, comp)
if err != nil {
return nil, err
}
@@ -146,12 +148,12 @@ func (p *Parser) GenerateAppFile(name string, app *v1alpha2.Application) (*Appfi
return appfile, nil
}
func (p *Parser) parseWorkload(comp v1alpha2.ApplicationComponent) (*Workload, error) {
func (p *Parser) parseWorkload(ctx context.Context, comp v1alpha2.ApplicationComponent) (*Workload, error) {
workload := new(Workload)
workload.Traits = []*Trait{}
workload.Name = comp.Name
workload.Type = comp.WorkloadType
templ, err := util.LoadTemplate(p.client, workload.Type, types.TypeWorkload)
templ, err := util.LoadTemplate(ctx, p.client, workload.Type, types.TypeWorkload)
if err != nil && !kerrors.IsNotFound(err) {
return nil, errors.WithMessagef(err, "fetch type of %s", comp.Name)
}
@@ -169,7 +171,7 @@ func (p *Parser) parseWorkload(comp v1alpha2.ApplicationComponent) (*Workload, e
if err != nil {
return nil, errors.Errorf("fail to parse properties of %s for %s", traitValue.Name, comp.Name)
}
trait, err := p.parseTrait(traitValue.Name, properties)
trait, err := p.parseTrait(ctx, traitValue.Name, properties)
if err != nil {
return nil, errors.WithMessagef(err, "component(%s) parse trait(%s)", comp.Name, traitValue.Name)
}
@@ -177,7 +179,7 @@ func (p *Parser) parseWorkload(comp v1alpha2.ApplicationComponent) (*Workload, e
workload.Traits = append(workload.Traits, trait)
}
for scopeType, instanceName := range comp.Scopes {
gvk, err := util.GetScopeGVK(p.client, p.dm, scopeType)
gvk, err := util.GetScopeGVK(ctx, p.client, p.dm, scopeType)
if err != nil {
return nil, err
}
@@ -189,8 +191,8 @@ func (p *Parser) parseWorkload(comp v1alpha2.ApplicationComponent) (*Workload, e
return workload, nil
}
func (p *Parser) parseTrait(name string, properties map[string]interface{}) (*Trait, error) {
templ, err := util.LoadTemplate(p.client, name, types.TypeTrait)
func (p *Parser) parseTrait(ctx context.Context, name string, properties map[string]interface{}) (*Trait, error) {
templ, err := util.LoadTemplate(ctx, p.client, name, types.TypeTrait)
if kerrors.IsNotFound(err) {
return nil, errors.Errorf("trait definition of %s not found", name)
}

View File

@@ -34,7 +34,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/oam/util"
@@ -240,7 +239,7 @@ var _ = Describe("Test application parser", func() {
},
}
appfile, err := NewApplicationParser(&tclient, nil).GenerateAppFile("test", &o)
appfile, err := NewApplicationParser(&tclient, nil).GenerateAppFile(context.TODO(), "test", &o)
Expect(err).ShouldNot(HaveOccurred())
Expect(equal(expectedExceptApp, appfile)).Should(BeTrue())
@@ -426,37 +425,33 @@ var _ = Describe("Test appFile parser", func() {
Name: "myweb",
Namespace: "default",
Labels: map[string]string{"application.oam.dev": "test"},
}, Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{
Object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"workload.oam.dev/type": "worker",
"app.oam.dev/component": "myweb",
"app.oam.dev/name": "test",
},
},
"spec": map[string]interface{}{
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "myweb"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{"labels": map[string]interface{}{"app.oam.dev/component": "myweb"}},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"command": []interface{}{"sleep", "1000"},
"image": "busybox",
"name": "myweb",
"env": []interface{}{
map[string]interface{}{"name": "c1", "value": "v1"},
map[string]interface{}{"name": "c2", "value": "v2"},
},
},
},
}}
expectWorkload := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"workload.oam.dev/type": "worker",
"app.oam.dev/component": "myweb",
"app.oam.dev/name": "test",
},
},
"spec": map[string]interface{}{
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "myweb"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{"labels": map[string]interface{}{"app.oam.dev/component": "myweb"}},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"command": []interface{}{"sleep", "1000"},
"image": "busybox",
"name": "myweb",
"env": []interface{}{
map[string]interface{}{"name": "c1", "value": "v1"},
map[string]interface{}{"name": "c2", "value": "v2"},
},
},
},
@@ -465,12 +460,30 @@ var _ = Describe("Test appFile parser", func() {
},
},
}
// assertion util cannot compare slices embedded in map correctly while slice order is not required
// e.g., .containers[0].env in this case
// as a workaround, prepare two expected targets covering all possible slice order
// if any one is satisfied, the equal assertion pass
expectWorkloadOptional := expectWorkload.DeepCopy()
unstructured.SetNestedSlice(expectWorkloadOptional.Object, []interface{}{
map[string]interface{}{
"command": []interface{}{"sleep", "1000"},
"image": "busybox",
"name": "myweb",
"env": []interface{}{
map[string]interface{}{"name": "c2", "value": "v2"},
map[string]interface{}{"name": "c1", "value": "v1"},
},
},
}, "spec", "template", "spec", "containers")
By(" built components' length must be 1")
Expect(len(components)).To(BeEquivalentTo(1))
Expect(components[0].ObjectMeta).To(BeEquivalentTo(expectComponent.ObjectMeta))
Expect(components[0].TypeMeta).To(BeEquivalentTo(expectComponent.TypeMeta))
logf.Log.Info(cmp.Diff(components[0].Spec.Workload.Object, expectComponent.Spec.Workload.Object))
Expect(assert.ObjectsAreEqual(components[0].Spec.Workload.Object, expectComponent.Spec.Workload.Object)).To(BeTrue())
Expect(components[0].Spec.Workload.Object).Should(SatisfyAny(
BeEquivalentTo(expectWorkload),
BeEquivalentTo(expectWorkloadOptional)))
})
})

View File

@@ -1,6 +1,7 @@
package commands
import (
"context"
"encoding/json"
"io/ioutil"
"path/filepath"
@@ -15,6 +16,7 @@ import (
"github.com/oam-dev/kubevela/pkg/appfile"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
type dryRunOptions struct {
@@ -52,7 +54,14 @@ func NewDryRunCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command
parser := appfile.NewApplicationParser(newClient, dm)
appFile, err := parser.GenerateAppFile(app.Name, app)
velaEnv, err := GetEnv(cmd)
if err != nil {
return err
}
ctx := oamutil.SetNnamespaceInCtx(context.Background(), velaEnv.Namespace)
appFile, err := parser.GenerateAppFile(ctx, app.Name, app)
if err != nil {
return errors.WithMessage(err, "generate appFile")
}

View File

@@ -8,7 +8,6 @@ import (
"os"
"time"
"github.com/openservicemesh/osm/pkg/cli"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/chart"
@@ -18,6 +17,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/hack/utils"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/plugins"
"github.com/oam-dev/kubevela/pkg/utils/helm"
@@ -48,6 +48,7 @@ type chartArgs struct {
imageRepo string
imageTag string
imagePullPolicy string
dependCheckWait string
more []string
}
@@ -131,6 +132,7 @@ func NewInstallCommand(c types.Args, chartContent string, ioStreams cmdutil.IOSt
flag.StringVarP(&i.chartArgs.imagePullPolicy, "image-pull-policy", "", "", "vela core image pull policy, this will align to chart value image.pullPolicy")
flag.StringVarP(&i.chartArgs.imageRepo, "image-repo", "", "", "vela core image repo, this will align to chart value image.repo")
flag.StringVarP(&i.chartArgs.imageTag, "image-tag", "", "", "vela core image repo, this will align to chart value image.tag")
flag.StringVarP(&i.chartArgs.dependCheckWait, "depend-check-wait", "", "", "depend-check-wait, this the time to wait for ApplicationConfiguration's dependent-resource ready")
flag.StringVarP(&i.waitReady, "wait", "w", "0s", "wait until vela-core is ready to serve, default will not wait")
flag.StringSliceVarP(&i.chartArgs.more, "set", "s", []string{}, "arguments for installing vela-core chart")
@@ -228,6 +230,9 @@ func (i *initCmd) resolveValues() (map[string]interface{}, error) {
if i.chartArgs.imagePullPolicy != "" {
valuesConfig = append(valuesConfig, fmt.Sprintf("image.pullPolicy=%s", i.chartArgs.imagePullPolicy))
}
if i.chartArgs.dependCheckWait != "" {
valuesConfig = append(valuesConfig, fmt.Sprintf("dependCheckWait=%s", i.chartArgs.dependCheckWait))
}
valuesConfig = append(valuesConfig, i.chartArgs.more...)
for _, val := range valuesConfig {
@@ -247,7 +252,7 @@ func InstallOamRuntime(chartPath, chartSource string, vals map[string]interface{
ioStreams.Infof("Use customized chart at: %s", chartPath)
chartRequested, err = loader.Load(chartPath)
} else {
chartRequested, err = cli.LoadChart(chartSource)
chartRequested, err = utils.LoadChart(chartSource)
if chartRequested != nil {
m, l := chartRequested.Metadata, len(chartRequested.Raw)
ioStreams.Infof("install chart %s, version %s, desc : %s, contains %d file\n", m.Name, m.Version, m.Description, l)

View File

@@ -16,6 +16,8 @@ limitations under the License.
package core_oam_dev
import "time"
// ApplyOnceOnlyMode enumerates ApplyOnceOnly modes.
type ApplyOnceOnlyMode string
@@ -26,12 +28,12 @@ const (
// ApplyOnceOnlyOn indicates workloads and traits should not be affected
// if no spec change is made in the ApplicationConfiguration.
ApplyOnceOnlyOn = "on"
ApplyOnceOnlyOn ApplyOnceOnlyMode = "on"
// ApplyOnceOnlyForce is a more strong case for ApplyOnceOnly, the workload
// and traits won't be affected if no spec change is made in the ApplicationConfiguration,
// even if the workload or trait has been deleted from cluster.
ApplyOnceOnlyForce = "force"
ApplyOnceOnlyForce ApplyOnceOnlyMode = "force"
)
// Args args used by controller
@@ -47,4 +49,13 @@ type Args struct {
// CustomRevisionHookURL is a webhook which will let oam-runtime to call with AC+Component info
// The webhook server will return a customized component revision for oam-runtime
CustomRevisionHookURL string
// LongWait is controller next reconcile interval time
LongWait time.Duration
// ConcurrentReconciles is the concurrent reconcile number of the controller
ConcurrentReconciles int
// DependCheckWait is the time to wait for ApplicationConfiguration's dependent-resource ready
DependCheckWait time.Duration
}

View File

@@ -37,6 +37,7 @@ import (
core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
// RolloutReconcileWaitTime is the time to wait before reconcile again an application still in rollout phase
@@ -93,7 +94,9 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
// parse template
appParser := appfile.NewApplicationParser(r.Client, r.dm)
appfile, err := appParser.GenerateAppFile(app.Name, app)
ctx = oamutil.SetNnamespaceInCtx(ctx, app.Namespace)
appfile, err := appParser.GenerateAppFile(ctx, app.Name, app)
if err != nil {
handler.l.Error(err, "[Handle Parse]")
app.Status.SetConditions(errorCondition("Parsed", err))

View File

@@ -651,7 +651,7 @@ var _ = Describe("Test Application Controller", func() {
ntd, otd := &v1alpha2.TraitDefinition{}, &v1alpha2.TraitDefinition{}
tDDefJson, _ := yaml.YAMLToJSON([]byte(tdDefYamlWithHttp))
Expect(json.Unmarshal(tDDefJson, ntd)).Should(BeNil())
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "scaler"}, otd)).Should(BeNil())
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: ntd.Name, Namespace: ntd.Namespace}, otd)).Should(BeNil())
ntd.ResourceVersion = otd.ResourceVersion
Expect(k8sClient.Update(ctx, ntd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
@@ -695,13 +695,13 @@ var _ = Describe("Test Application Controller", func() {
nwd, owd := &v1alpha2.WorkloadDefinition{}, &v1alpha2.WorkloadDefinition{}
wDDefJson, _ := yaml.YAMLToJSON([]byte(wDDefWithHealthYaml))
Expect(json.Unmarshal(wDDefJson, nwd)).Should(BeNil())
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "worker"}, owd)).Should(BeNil())
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: nwd.Name, Namespace: nwd.Namespace}, owd)).Should(BeNil())
nwd.ResourceVersion = owd.ResourceVersion
Expect(k8sClient.Update(ctx, nwd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
ntd, otd := &v1alpha2.TraitDefinition{}, &v1alpha2.TraitDefinition{}
tDDefJson, _ := yaml.YAMLToJSON([]byte(tDDefWithHealthYaml))
Expect(json.Unmarshal(tDDefJson, ntd)).Should(BeNil())
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "scaler"}, otd)).Should(BeNil())
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: ntd.Name, Namespace: ntd.Namespace}, otd)).Should(BeNil())
ntd.ResourceVersion = otd.ResourceVersion
Expect(k8sClient.Update(ctx, ntd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
compName := "myweb-health"
@@ -995,7 +995,7 @@ const (
kind: ScopeDefinition
metadata:
name: healthscopes.core.oam.dev
namespace: default
namespace: vela-system
spec:
workloadRefsPath: spec.workloadRefs
allowComponentOverlap: true
@@ -1007,6 +1007,7 @@ apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: worker
namespace: vela-system
annotations:
definition.oam.dev/description: "Long-running scalable backend worker without network endpoint"
spec:
@@ -1066,6 +1067,7 @@ spec:
kind: WorkloadDefinition
metadata:
name: webserver
namespace: vela-system
annotations:
definition.oam.dev/description: "webserver was composed by deployment and service"
spec:
@@ -1157,6 +1159,7 @@ apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: worker
namespace: vela-system
annotations:
definition.oam.dev/description: "Long-running scalable backend worker without network endpoint"
spec:
@@ -1217,6 +1220,7 @@ spec:
kind: WorkloadDefinition
metadata:
name: nworker
namespace: vela-system
annotations:
definition.oam.dev/description: "Describes long-running, scalable, containerized services that running at backend. They do NOT have network endpoint to receive external network traffic."
spec:
@@ -1227,57 +1231,59 @@ spec:
isHealth: (context.output.status.readyReplicas > 0) && (context.output.status.readyReplicas == context.output.status.replicas)
customStatus: |-
message: "type: " + context.output.spec.template.spec.containers[0].image + ",\t enemies:" + context.outputs.gameconfig.data.enemies
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
schematic:
cue:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
envFrom: [{
configMapRef: name: context.name + "game-config"
}]
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
spec: {
containers: [{
name: context.name
image: parameter.image
envFrom: [{
configMapRef: name: context.name + "game-config"
}]
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
outputs: gameconfig: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: context.name + "game-config"
}
data: {
enemies: parameter.enemies
lives: parameter.lives
}
}
outputs: gameconfig: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: context.name + "game-config"
}
data: {
enemies: parameter.enemies
lives: parameter.lives
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
lives: string
enemies: string
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
lives: string
enemies: string
}
`
tDDefYaml = `
apiVersion: core.oam.dev/v1alpha2
@@ -1286,6 +1292,7 @@ metadata:
annotations:
definition.oam.dev/description: "Manually scale the app"
name: scaler
namespace: vela-system
spec:
appliesToWorkloads:
- webservice
@@ -1315,6 +1322,7 @@ metadata:
annotations:
definition.oam.dev/description: "Manually scale the app"
name: scaler
namespace: vela-system
spec:
appliesToWorkloads:
- webservice
@@ -1359,6 +1367,7 @@ metadata:
annotations:
definition.oam.dev/description: "Manually scale the app"
name: scaler
namespace: vela-system
spec:
appliesToWorkloads:
- webservice
@@ -1387,54 +1396,57 @@ spec:
kind: TraitDefinition
metadata:
name: ingress
namespace: vela-system
spec:
status:
customStatus: |-
message: "type: "+ context.outputs.service.spec.type +",\t clusterIP:"+ context.outputs.service.spec.clusterIP+",\t ports:"+ "\(context.outputs.service.spec.ports[0].port)"+",\t domain"+context.outputs.ingress.spec.rules[0].host
healthPolicy: |
isHealth: len(context.outputs.service.spec.clusterIP) > 0
template: |
parameter: {
domain: string
http: [string]: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
spec: {
selector:
app: context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
},
]
}
}
outputs: ingress: {
apiVersion: "networking.k8s.io/v1beta1"
kind: "Ingress"
metadata:
name: context.name
spec: {
rules: [{
host: parameter.domain
http: {
paths: [
for k, v in parameter.http {
path: k
backend: {
serviceName: context.name
servicePort: v
}
},
]
}
}]
}
}
schematic:
cue:
template: |
parameter: {
domain: string
http: [string]: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
spec: {
selector:
app: context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
},
]
}
}
outputs: ingress: {
apiVersion: "networking.k8s.io/v1beta1"
kind: "Ingress"
metadata:
name: context.name
spec: {
rules: [{
host: parameter.domain
http: {
paths: [
for k, v in parameter.http {
path: k
backend: {
serviceName: context.name
servicePort: v
}
},
]
}
}]
}
}
`
)

View File

@@ -17,12 +17,16 @@ limitations under the License.
package application
import (
"context"
"os"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
@@ -61,9 +65,16 @@ var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
By("bootstrapping test environment")
var yamlPath string
if _, set := os.LookupEnv("COMPATIBILITY_TEST"); set {
yamlPath = "../../../../../test/compatibility-test/testdata"
} else {
yamlPath = filepath.Join("../../../../..", "charts", "vela-core", "crds")
}
logf.Log.Info("start application suit test", "yaml_path", yamlPath)
testEnv = &envtest.Environment{
UseExistingCluster: pointer.BoolPtr(false),
CRDDirectoryPaths: []string{filepath.Join("../../../../..", "charts", "vela-core", "crds")},
CRDDirectoryPaths: []string{yamlPath},
}
var err error
@@ -89,6 +100,8 @@ var _ = BeforeSuite(func(done Done) {
Scheme: testScheme,
dm: dm,
}
definitonNs := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}}
Expect(k8sClient.Create(context.Background(), definitonNs.DeepCopy())).Should(BeNil())
close(done)
}, 60)

View File

@@ -31,11 +31,13 @@ import (
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
@@ -50,9 +52,7 @@ import (
const (
reconcileTimeout = 1 * time.Minute
dependCheckWait = 10 * time.Second
shortWait = 30 * time.Second
longWait = 1 * time.Minute
)
var errResult = reconcile.Result{RequeueAfter: shortWait}
@@ -92,7 +92,12 @@ func Setup(mgr ctrl.Manager, args core.Args, l logging.Logger) error {
}
name := "oam/" + strings.ToLower(v1alpha2.ApplicationConfigurationGroupKind)
return ctrl.NewControllerManagedBy(mgr).
builder := ctrl.NewControllerManagedBy(mgr)
builder.WithOptions(controller.Options{
MaxConcurrentReconciles: args.ConcurrentReconciles,
})
return builder.
Named(name).
For(&v1alpha2.ApplicationConfiguration{}).
Watches(&source.Kind{Type: &v1alpha2.Component{}}, &ComponentHandler{
@@ -102,9 +107,11 @@ func Setup(mgr ctrl.Manager, args core.Args, l logging.Logger) error {
CustomRevisionHookURL: args.CustomRevisionHookURL,
}).
Complete(NewReconciler(mgr, dm,
WithLogger(l.WithValues("controller", name)),
l.WithValues("controller", name),
WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))),
WithApplyOnceOnlyMode(args.ApplyMode)))
WithApplyOnceOnlyMode(args.ApplyMode),
WithLongWaitTime(args.LongWait),
WithDependCheckWait(args.DependCheckWait)))
}
// An OAMApplicationReconciler reconciles OAM ApplicationConfigurations by rendering and
@@ -120,6 +127,8 @@ type OAMApplicationReconciler struct {
preHooks map[string]ControllerHooks
postHooks map[string]ControllerHooks
applyOnceOnlyMode core.ApplyOnceOnlyMode
longWait time.Duration
dependCheckWait time.Duration
}
// A ReconcilerOption configures a Reconciler.
@@ -148,13 +157,6 @@ func WithGarbageCollector(gc GarbageCollector) ReconcilerOption {
}
}
// WithLogger specifies how the Reconciler should log messages.
func WithLogger(l logging.Logger) ReconcilerOption {
return func(r *OAMApplicationReconciler) {
r.log = l
}
}
// WithRecorder specifies how the Reconciler should record events.
func WithRecorder(er event.Recorder) ReconcilerOption {
return func(r *OAMApplicationReconciler) {
@@ -184,9 +186,23 @@ func WithApplyOnceOnlyMode(mode core.ApplyOnceOnlyMode) ReconcilerOption {
}
}
// WithLongWaitTime set next reconcile time interval
func WithLongWaitTime(longWait time.Duration) ReconcilerOption {
return func(r *OAMApplicationReconciler) {
r.longWait = longWait
}
}
// WithDependCheckWait set depend check wait
func WithDependCheckWait(dependCheckWait time.Duration) ReconcilerOption {
return func(r *OAMApplicationReconciler) {
r.dependCheckWait = dependCheckWait
}
}
// NewReconciler returns an OAMApplicationReconciler that reconciles ApplicationConfigurations
// by rendering and instantiating their Components and Traits.
func NewReconciler(m ctrl.Manager, dm discoverymapper.DiscoveryMapper, o ...ReconcilerOption) *OAMApplicationReconciler {
func NewReconciler(m ctrl.Manager, dm discoverymapper.DiscoveryMapper, log logging.Logger, o ...ReconcilerOption) *OAMApplicationReconciler {
r := &OAMApplicationReconciler{
client: m.GetClient(),
scheme: m.GetScheme(),
@@ -198,12 +214,12 @@ func NewReconciler(m ctrl.Manager, dm discoverymapper.DiscoveryMapper, o ...Reco
trait: ResourceRenderFn(renderTrait),
},
workloads: &workloads{
applicator: apply.NewAPIApplicator(m.GetClient()),
applicator: apply.NewAPIApplicator(m.GetClient(), log),
rawClient: m.GetClient(),
dm: dm,
},
gc: GarbageCollectorFn(eligible),
log: logging.NewNopLogger(),
log: log,
record: event.NewNopRecorder(),
preHooks: make(map[string]ControllerHooks),
postHooks: make(map[string]ControllerHooks),
@@ -236,6 +252,7 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
}
acPatch := ac.DeepCopy()
ctx = util.SetNnamespaceInCtx(ctx, ac.Namespace)
if ac.ObjectMeta.DeletionTimestamp.IsZero() {
if registerFinalizers(ac) {
log.Debug("Register new finalizers", "finalizers", ac.ObjectMeta.Finalizers)
@@ -252,6 +269,14 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
return reconcile.Result{}, errors.Wrap(r.client.Update(ctx, ac), errUpdateAppConfigStatus)
}
// make sure this is the last functional defer function to be called
defer func() {
// Make sure if error occurs, reconcile will not happen too frequency
if returnErr != nil {
result.RequeueAfter = 0
}
}()
// execute the posthooks at the end no matter what
defer func() {
updateObservedGeneration(ac)
@@ -268,11 +293,6 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
r.record.Event(ac, event.Normal(reasonExecutePosthook, "Successfully executed a posthook", "posthook name", name))
}
returnErr = errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
// Make sure if error occurs, reconcile will not happen too frequency
if returnErr != nil && result.RequeueAfter < shortWait {
result.RequeueAfter = shortWait
}
}()
// execute the prehooks
@@ -299,7 +319,7 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
log.Debug("Successfully rendered components", "workloads", len(workloads))
r.record.Event(ac, event.Normal(reasonRenderComponents, "Successfully rendered components", "workloads", strconv.Itoa(len(workloads))))
applyOpts := []apply.ApplyOption{apply.MustBeControllableBy(ac.GetUID()), applyOnceOnly(ac, r.applyOnceOnlyMode)}
applyOpts := []apply.ApplyOption{apply.MustBeControllableBy(ac.GetUID()), applyOnceOnly(ac, r.applyOnceOnlyMode, log)}
if err := r.workloads.Apply(ctx, ac.Status.Workloads, workloads, applyOpts...); err != nil {
log.Debug("Cannot apply components", "error", err, "requeue-after", time.Now().Add(shortWait))
r.record.Event(ac, event.Warning(reasonCannotApplyComponents, err))
@@ -320,8 +340,15 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
log := log.WithValues("kind", e.GetKind(), "name", e.GetName())
record := r.record.WithAnnotations("kind", e.GetKind(), "name", e.GetName())
err := r.confirmDeleteOnApplyOnceMode(ctx, ac.GetNamespace(), &e)
if err != nil {
log.Debug("confirm component can't be garbage collected", "error", err)
record.Event(ac, event.Warning(reasonCannotGGComponents, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errGCComponent)))
return errResult, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
}
if err := r.client.Delete(ctx, &e); resource.IgnoreNotFound(err) != nil {
log.Debug("Cannot garbage collect component", "error", err, "requeue-after", time.Now().Add(shortWait))
log.Debug("Cannot garbage collect component", "error", err)
record.Event(ac, event.Warning(reasonCannotGGComponents, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errGCComponent)))
return errResult, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
@@ -334,9 +361,9 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
r.updateStatus(ctx, ac, acPatch, workloads)
ac.Status.Dependency = v1alpha2.DependencyStatus{}
waitTime := longWait
waitTime := r.longWait
if len(depStatus.Unsatisfied) != 0 {
waitTime = dependCheckWait
waitTime = r.dependCheckWait
ac.Status.Dependency = *depStatus
}
@@ -344,6 +371,41 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
return reconcile.Result{RequeueAfter: waitTime}, nil
}
// confirmDeleteOnApplyOnceMode will confirm whether the workload can be delete or not in apply once only enabled mode
// currently only workload replicas with 0 can be delete
func (r *OAMApplicationReconciler) confirmDeleteOnApplyOnceMode(ctx context.Context, namespace string, u *unstructured.Unstructured) error {
if r.applyOnceOnlyMode == core.ApplyOnceOnlyOff {
return nil
}
getU := u.DeepCopy()
err := r.client.Get(ctx, client.ObjectKey{Name: u.GetName(), Namespace: namespace}, getU)
if err != nil {
// no need to check if workload not found
return resource.IgnoreNotFound(err)
}
// only check for workload
if labels := getU.GetLabels(); labels == nil || labels[oam.LabelOAMResourceType] != oam.ResourceTypeWorkload {
return nil
}
paved := fieldpath.Pave(getU.Object)
// TODO: add more kinds of workload replica check here if needed
// "spec.replicas" maybe not accurate for all kinds of workload, but it work for most of them(including Deployment/StatefulSet/CloneSet).
// For workload which don't align with the `spec.replicas` schema, the check won't work
replicas, err := paved.GetInteger("spec.replicas")
if err != nil {
// it's possible for workload without the `spec.replicas`, it's omitempty
if strings.Contains(err.Error(), "no such field") {
return nil
}
return errors.WithMessage(err, "fail to get 'spec.replicas' from workload")
}
if replicas > 0 {
return errors.Errorf("can't delete workload with replicas %d in apply once only mode", replicas)
}
return nil
}
// UpdateStatus updates v1alpha2.ApplicationConfiguration's Status with retry.RetryOnConflict
func (r *OAMApplicationReconciler) UpdateStatus(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, opts ...client.UpdateOption) error {
status := ac.DeepCopy().Status
@@ -361,7 +423,6 @@ func (r *OAMApplicationReconciler) updateStatus(ctx context.Context, ac, acPatch
historyWorkloads := make([]v1alpha2.HistoryWorkload, 0)
for i, w := range workloads {
ac.Status.Workloads[i] = workloads[i].Status()
ac.Status.Workloads[i].ObservedGeneration = ac.GetGeneration()
if !w.RevisionEnabled {
continue
}
@@ -398,6 +459,17 @@ func updateObservedGeneration(ac *v1alpha2.ApplicationConfiguration) {
if ac.Status.ObservedGeneration != ac.Generation {
ac.Status.ObservedGeneration = ac.Generation
}
for i, w := range ac.Status.Workloads {
// only the workload meet all dependency can mean the generation applied successfully
if w.AppliedComponentRevision != w.ComponentRevisionName && !w.DependencyUnsatisfied {
ac.Status.Workloads[i].AppliedComponentRevision = w.ComponentRevisionName
}
for j, t := range w.Traits {
if t.AppliedGeneration != ac.Generation && !t.DependencyUnsatisfied {
ac.Status.Workloads[i].Traits[j].AppliedGeneration = ac.Generation
}
}
}
}
func patchExtraStatusField(acStatus *v1alpha2.ApplicationConfigurationStatus, acPatchStatus v1alpha2.ApplicationConfigurationStatus) {
@@ -485,6 +557,7 @@ func (w Workload) Status() v1alpha2.WorkloadStatus {
acw := v1alpha2.WorkloadStatus{
ComponentName: w.ComponentName,
ComponentRevisionName: w.ComponentRevisionName,
DependencyUnsatisfied: w.HasDep,
Reference: v1alpha1.TypedReference{
APIVersion: w.Workload.GetAPIVersion(),
Kind: w.Workload.GetKind(),
@@ -502,6 +575,7 @@ func (w Workload) Status() v1alpha2.WorkloadStatus {
Kind: w.Traits[i].Object.GetKind(),
Name: w.Traits[i].Object.GetName(),
}
acw.Traits[i].DependencyUnsatisfied = tr.HasDep
}
for i, s := range w.Scopes {
acw.Scopes[i].Reference = v1alpha1.TypedReference{
@@ -529,9 +603,20 @@ func (fn GarbageCollectorFn) Eligible(namespace string, ws []v1alpha2.WorkloadSt
}
// IsRevisionWorkload check is a workload is an old revision Workload which shouldn't be garbage collected.
// TODO(wonderflow): Do we have a better way to recognise it's a revisionWorkload which can't be garbage collected by AppConfig?
func IsRevisionWorkload(status v1alpha2.WorkloadStatus) bool {
return strings.HasPrefix(status.Reference.Name, status.ComponentName+"-")
func IsRevisionWorkload(status v1alpha2.WorkloadStatus, w []Workload) bool {
if strings.HasPrefix(status.Reference.Name, status.ComponentName+"-") {
// for compatibility, keep the old way
return true
}
// check all workload, with same componentName
for _, wr := range w {
if wr.ComponentName == status.ComponentName {
return wr.RevisionEnabled
}
}
// component not found, should be deleted
return false
}
func eligible(namespace string, ws []v1alpha2.WorkloadStatus, w []Workload) []unstructured.Unstructured {
@@ -555,7 +640,7 @@ func eligible(namespace string, ws []v1alpha2.WorkloadStatus, w []Workload) []un
eligible := make([]unstructured.Unstructured, 0)
for _, s := range ws {
if !applied[s.Reference] && !IsRevisionWorkload(s) {
if !applied[s.Reference] && !IsRevisionWorkload(s, w) {
w := &unstructured.Unstructured{}
w.SetAPIVersion(s.Reference.APIVersion)
w.SetKind(s.Reference.Kind)
@@ -591,12 +676,11 @@ func (e *GenerationUnchanged) Error() string {
// applyOnceOnly is an ApplyOption that controls the applying mechanism for workload and trait.
// More detail refers to the ApplyOnceOnlyMode type annotation
func applyOnceOnly(ac *v1alpha2.ApplicationConfiguration, mode core.ApplyOnceOnlyMode) apply.ApplyOption {
func applyOnceOnly(ac *v1alpha2.ApplicationConfiguration, mode core.ApplyOnceOnlyMode, log logging.Logger) apply.ApplyOption {
return func(_ context.Context, existing, desired runtime.Object) error {
if mode == core.ApplyOnceOnlyOff {
return nil
}
d, _ := desired.(metav1.Object)
if d == nil {
return errors.Errorf("cannot access metadata of object being applied: %q",
@@ -608,6 +692,7 @@ func applyOnceOnly(ac *v1alpha2.ApplicationConfiguration, mode core.ApplyOnceOnl
dLabels[oam.LabelOAMResourceType] != oam.ResourceTypeTrait {
// this ApplyOption only works for workload and trait
// skip if the resource is not workload nor trait, e.g., scope
log.Info("ignore apply only once check, because resourceType is not workload or trait", oam.LabelOAMResourceType, dLabels[oam.LabelOAMResourceType])
return nil
}
@@ -615,41 +700,54 @@ func applyOnceOnly(ac *v1alpha2.ApplicationConfiguration, mode core.ApplyOnceOnl
if existing == nil {
if mode != core.ApplyOnceOnlyForce {
// non-force mode will always create the resource if not exist.
log.Info("apply only once with mode:" + string(mode) + ", but old resource not exist, will create a new one")
return nil
}
createdBefore := false
var appliedRevision, appliedGeneration string
for _, w := range ac.Status.Workloads {
// traverse recorded workloads to find the one matching applied resource
if w.Reference.GetObjectKind().GroupVersionKind() == desired.GetObjectKind().GroupVersionKind() &&
w.Reference.Name == d.GetName() {
// the workload matches applied resource
createdBefore = true
// for workload, when revision enabled, only when revision changed that can trigger to create a new one
if dLabels[oam.LabelOAMResourceType] == oam.ResourceTypeWorkload && w.AppliedComponentRevision == dLabels[oam.LabelAppComponentRevision] {
// the revision is not changed, so return an error to abort creating it
return &GenerationUnchanged{}
}
appliedRevision = w.AppliedComponentRevision
break
}
if !createdBefore {
// the workload is not matched, then traverse its traits to find matching one
for _, t := range w.Traits {
if t.Reference.GetObjectKind().GroupVersionKind() == desired.GetObjectKind().GroupVersionKind() &&
t.Reference.Name == d.GetName() {
// the trait matches applied resource
createdBefore = true
// the workload is not matched, then traverse its traits to find matching one
for _, t := range w.Traits {
if t.Reference.GetObjectKind().GroupVersionKind() == desired.GetObjectKind().GroupVersionKind() &&
t.Reference.Name == d.GetName() {
// the trait matches applied resource
createdBefore = true
// the resource was created before and appConfig status recorded the resource version applied
// if recorded AppliedGeneration and ComponentRevisionName both equal to the applied resource's,
// that means its spec is not changed
if dLabels[oam.LabelOAMResourceType] == oam.ResourceTypeTrait &&
w.ComponentRevisionName == dLabels[oam.LabelAppComponentRevision] &&
strconv.FormatInt(t.AppliedGeneration, 10) == dAnnots[oam.AnnotationAppGeneration] {
// the revision is not changed, so return an error to abort creating it
return &GenerationUnchanged{}
}
appliedGeneration = strconv.FormatInt(t.AppliedGeneration, 10)
break
}
}
// don't use if-else here because it will miss the case that the resource is a trait
if createdBefore {
// the resource was created before and appconfig status recorded the resource version applied
// if recored ObservedGeneration and ComponentRevisionName both equal to the applied resource's,
// that means its spec is not changed
if (strconv.Itoa(int(w.ObservedGeneration)) != dAnnots[oam.AnnotationAppGeneration]) ||
(w.ComponentRevisionName != dLabels[oam.LabelAppComponentRevision]) {
// its spec is changed, so re-create the resource
return nil
}
// its spec is not changed, so return an error to abort creating it
return &GenerationUnchanged{}
}
}
var message = "apply only once with mode: force, but resource not created before, will create new"
if createdBefore {
message = "apply only once with mode: force, but resource updated, will create new"
}
log.Info(message, "appConfig", ac.Name, "gvk", desired.GetObjectKind().GroupVersionKind(), "name", d.GetName(),
"resourceType", dLabels[oam.LabelOAMResourceType], "appliedCompRevision", appliedRevision, "labeledCompRevision", dLabels[oam.LabelAppComponentRevision],
"appliedGeneration", appliedGeneration, "labeledGeneration", dAnnots[oam.AnnotationAppGeneration])
// no recorded workloads nor traits matches the applied resource
// that means the resource is not created before, so create it
return nil
@@ -662,10 +760,13 @@ func applyOnceOnly(ac *v1alpha2.ApplicationConfiguration, mode core.ApplyOnceOnl
existing.GetObjectKind().GroupVersionKind())
}
eLabels := e.GetLabels()
// if existing reource's (observed)AppConfigGeneration and ComponentRevisionName both equal to the applied one's,
// if existing resource's (observed)AppConfigGeneration and ComponentRevisionName both equal to the applied one's,
// that means its spec is not changed
if (e.GetAnnotations()[oam.AnnotationAppGeneration] != dAnnots[oam.AnnotationAppGeneration]) ||
(eLabels[oam.LabelAppComponentRevision] != dLabels[oam.LabelAppComponentRevision]) {
log.Info("apply only once with mode: "+string(mode)+", but new generation or revision created, will create new",
oam.AnnotationAppGeneration, e.GetAnnotations()[oam.AnnotationAppGeneration]+"/"+dAnnots[oam.AnnotationAppGeneration],
oam.LabelAppComponentRevision, eLabels[oam.LabelAppComponentRevision]+"/"+dLabels[oam.LabelAppComponentRevision])
// its spec is changed, so apply new configuration to it
return nil
}

View File

@@ -183,6 +183,7 @@ func TestReconciler(t *testing.T) {
WithRenderer(ComponentRenderFn(func(_ context.Context, _ *v1alpha2.ApplicationConfiguration) ([]Workload, *v1alpha2.DependencyStatus, error) {
return nil, &v1alpha2.DependencyStatus{}, errBoom
})),
WithDependCheckWait(10 * time.Second),
},
},
want: want{
@@ -212,6 +213,7 @@ func TestReconciler(t *testing.T) {
WithApplicator(WorkloadApplyFns{ApplyFn: func(_ context.Context, _ []v1alpha2.WorkloadStatus, _ []Workload, _ ...apply.ApplyOption) error {
return errBoom
}}),
WithDependCheckWait(10 * time.Second),
},
},
want: want{
@@ -245,6 +247,7 @@ func TestReconciler(t *testing.T) {
WithGarbageCollector(GarbageCollectorFn(func(_ string, _ []v1alpha2.WorkloadStatus, _ []Workload) []unstructured.Unstructured {
return []unstructured.Unstructured{*workload}
})),
WithDependCheckWait(10 * time.Second),
},
},
want: want{
@@ -308,10 +311,11 @@ func TestReconciler(t *testing.T) {
WithGarbageCollector(GarbageCollectorFn(func(_ string, _ []v1alpha2.WorkloadStatus, _ []Workload) []unstructured.Unstructured {
return []unstructured.Unstructured{*trait}
})),
WithDependCheckWait(10 * time.Second),
},
},
want: want{
result: reconcile.Result{RequeueAfter: dependCheckWait},
result: reconcile.Result{RequeueAfter: 10 * time.Second},
},
},
"FailedPreHook": {
@@ -352,6 +356,7 @@ func TestReconciler(t *testing.T) {
WithPosthook("postHook", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: shortWait}, nil
})),
WithDependCheckWait(10 * time.Second),
},
},
want: want{
@@ -422,6 +427,7 @@ func TestReconciler(t *testing.T) {
WithPosthook("preHookFailed", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: 15 * time.Second}, errBoom
})),
WithDependCheckWait(10 * time.Second),
},
},
want: want{
@@ -472,6 +478,7 @@ func TestReconciler(t *testing.T) {
WithPosthook("preHookFailed", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: 15 * time.Second}, errBoom
})),
WithDependCheckWait(10 * time.Second),
},
},
want: want{
@@ -539,10 +546,12 @@ func TestReconciler(t *testing.T) {
WithPosthook("postHook", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: shortWait}, nil
})),
WithLongWaitTime(1 * time.Minute),
WithDependCheckWait(10 * time.Second),
},
},
want: want{
result: reconcile.Result{RequeueAfter: longWait},
result: reconcile.Result{RequeueAfter: 1 * time.Minute},
},
},
"RegisterFinalizer": {
@@ -583,6 +592,9 @@ func TestReconciler(t *testing.T) {
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
},
},
o: []ReconcilerOption{
WithDependCheckWait(10 * time.Second),
},
},
want: want{
result: reconcile.Result{},
@@ -615,6 +627,9 @@ func TestReconciler(t *testing.T) {
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
},
},
o: []ReconcilerOption{
WithDependCheckWait(10 * time.Second),
},
},
want: want{
result: reconcile.Result{},
@@ -651,6 +666,7 @@ func TestReconciler(t *testing.T) {
WithApplicator(WorkloadApplyFns{FinalizeFn: func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) error {
return errBoom
}}),
WithDependCheckWait(10 * time.Second),
},
},
want: want{
@@ -661,7 +677,7 @@ func TestReconciler(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewReconciler(tc.args.m, nil, tc.args.o...)
r := NewReconciler(tc.args.m, nil, logging.NewNopLogger(), tc.args.o...)
got, err := r.Reconcile(reconcile.Request{})
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
@@ -847,9 +863,18 @@ func TestEligible(t *testing.T) {
}
func TestIsRevisionWorkload(t *testing.T) {
if true != IsRevisionWorkload(v1alpha2.WorkloadStatus{ComponentName: "compName", Reference: runtimev1alpha1.TypedReference{Name: "compName-rev1"}}) {
if true != IsRevisionWorkload(v1alpha2.WorkloadStatus{ComponentName: "compName", Reference: runtimev1alpha1.TypedReference{Name: "compName-rev1"}}, nil) {
t.Error("workloadName has componentName as prefix is revisionWorkload")
}
if true != IsRevisionWorkload(v1alpha2.WorkloadStatus{ComponentName: "compName", Reference: runtimev1alpha1.TypedReference{Name: "speciedName"}}, []Workload{{ComponentName: "compName", RevisionEnabled: true}}) {
t.Error("workloadName has componentName same and revisionEnabled is revisionWorkload")
}
if false != IsRevisionWorkload(v1alpha2.WorkloadStatus{ComponentName: "compName", Reference: runtimev1alpha1.TypedReference{Name: "speciedName"}}, []Workload{{ComponentName: "compName", RevisionEnabled: false}}) {
t.Error("workloadName has componentName same and revisionEnabled is false")
}
if false != IsRevisionWorkload(v1alpha2.WorkloadStatus{ComponentName: "compName", Reference: runtimev1alpha1.TypedReference{Name: "speciedName"}}, []Workload{{ComponentName: "compName-notmatch", RevisionEnabled: true}}) {
t.Error("workload with no prefix and no componentName match is not revisionEnabled ")
}
}
func TestDependency(t *testing.T) {
@@ -1628,7 +1653,7 @@ func TestUpdateStatus(t *testing.T) {
},
}
r := NewReconciler(m, nil)
r := NewReconciler(m, nil, logging.NewNopLogger())
ac := &v1alpha2.ApplicationConfiguration{}
err := r.client.Get(context.Background(), types.NamespacedName{Name: "example-appconfig"}, ac)

View File

@@ -136,13 +136,12 @@ spec:
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appName}, ac)
}, 3*time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile")
reconcileRetry(reconciler, req)
By("Check workload created successfully")
Eventually(func() error {
By("Reconcile")
reconcileRetry(reconciler, req)
return k8sClient.Get(ctx, workloadKey, &workload)
}, 5*time.Second, 300*time.Millisecond).Should(BeNil())
}, 5*time.Second, time.Second).Should(BeNil())
By("Check reconcile again and no error will happen")
reconcileRetry(reconciler, req)
@@ -223,9 +222,14 @@ spec:
By("Check new trait CR is applied")
scale := v1alpha2.ManualScalerTrait{}
scaleKey := client.ObjectKey{Name: scaleName, Namespace: namespace}
err = k8sClient.Get(ctx, scaleKey, &scale)
Expect(err).Should(BeNil())
Expect(scale.Spec.ReplicaCount).Should(Equal(int32(3)))
Eventually(func() int32 {
By("Reconcile")
reconcileRetry(reconciler, req)
if err := k8sClient.Get(ctx, scaleKey, &scale); err != nil {
return 0
}
return scale.Spec.ReplicaCount
}, 5*time.Second, time.Second).Should(Equal(int32(3)))
})
AfterEach(func() {

View File

@@ -83,6 +83,9 @@ type workloads struct {
}
func (a *workloads) Apply(ctx context.Context, status []v1alpha2.WorkloadStatus, w []Workload, ao ...apply.ApplyOption) error {
if len(w) == 0 {
return errors.New("The number of workloads in appConfig is 0 ")
}
// they are all in the same namespace
var namespace = w[0].Workload.GetNamespace()
for _, wl := range w {

View File

@@ -4,6 +4,11 @@ import (
"context"
"time"
"github.com/crossplane/crossplane-runtime/pkg/logging"
ctrl "sigs.k8s.io/controller-runtime"
"k8s.io/apimachinery/pkg/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -141,8 +146,7 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile")
Expect(func() error { _, err := reconciler.Reconcile(req); return err }()).Should(BeNil())
time.Sleep(3 * time.Second)
reconcileRetry(reconciler, req)
})
AfterEach(func() {
@@ -166,6 +170,8 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
By("Get workload instance & Check workload spec")
cwObj := v1alpha2.ContainerizedWorkload{}
Eventually(func() error {
By("Reconcile")
reconcileRetry(reconciler, req)
return k8sClient.Get(ctx, cwObjKey, &cwObj)
}, 5*time.Second, time.Second).Should(BeNil())
Expect(cwObj.Spec.Containers[0].Image).Should(Equal(image1))
@@ -267,6 +273,8 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
By("Get workload instance & Check workload spec")
cwObj := v1alpha2.ContainerizedWorkload{}
Eventually(func() error {
By("Reconcile")
reconcileRetry(reconciler, req)
return k8sClient.Get(ctx, cwObjKey, &cwObj)
}, 5*time.Second, time.Second).Should(BeNil())
@@ -285,6 +293,375 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
})
When("ApplyOnceOnlyForce is enabled", func() {
It("tests the situation where workload is not applied at the first because of unsatisfied dependency",
func() {
componentHandler := &ComponentHandler{Client: k8sClient, RevisionLimit: 100, Logger: logging.NewLogrLogger(ctrl.Log.WithName("component-handler"))}
By("Enable ApplyOnceOnlyForce")
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
tempFoo := &unstructured.Unstructured{}
tempFoo.SetAPIVersion("example.com/v1")
tempFoo.SetKind("Foo")
tempFoo.SetNamespace(namespace)
inName := "data-input"
inputWorkload := &unstructured.Unstructured{}
inputWorkload.SetAPIVersion("example.com/v1")
inputWorkload.SetKind("Foo")
inputWorkload.SetNamespace(namespace)
inputWorkload.SetName(inName)
compInName := "comp-in"
compIn := v1alpha2.Component{
ObjectMeta: metav1.ObjectMeta{
Name: compInName,
Namespace: namespace,
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{
Object: inputWorkload,
},
},
}
outName := "data-output"
outputTrait := tempFoo.DeepCopy()
outputTrait.SetName(outName)
acWithDepName := "ac-dep"
acWithDep := v1alpha2.ApplicationConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: acWithDepName,
Namespace: namespace,
},
Spec: v1alpha2.ApplicationConfigurationSpec{
Components: []v1alpha2.ApplicationConfigurationComponent{
{
ComponentName: compInName,
DataInputs: []v1alpha2.DataInput{
{
ValueFrom: v1alpha2.DataInputValueFrom{
DataOutputName: "trait-output",
},
ToFieldPaths: []string{"spec.key"},
},
},
Traits: []v1alpha2.ComponentTrait{{
Trait: runtime.RawExtension{Object: outputTrait},
DataOutputs: []v1alpha2.DataOutput{{
Name: "trait-output",
FieldPath: "status.key",
}},
},
},
},
},
},
}
By("Create Component")
Expect(k8sClient.Create(ctx, &compIn)).Should(Succeed())
cmp := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, cmp)).Should(Succeed())
cmpV1 := cmp.DeepCopy()
By("component handler will automatically create controller revision")
Expect(func() bool {
_, ok := componentHandler.createControllerRevision(cmpV1, cmpV1)
return ok
}()).Should(BeTrue())
By("Creat appConfig & check successfully")
Expect(k8sClient.Create(ctx, &acWithDep)).Should(Succeed())
Eventually(func() error {
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile & check successfully")
reqDep := reconcile.Request{
NamespacedName: client.ObjectKey{Namespace: namespace, Name: acWithDepName},
}
Eventually(func() bool {
reconcileRetry(reconciler, reqDep)
acWithDep = v1alpha2.ApplicationConfiguration{}
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep); err != nil {
return false
}
return len(acWithDep.Status.Workloads) == 1
}, time.Second, 300*time.Millisecond).Should(BeTrue())
// because dependency is not satisfied so the workload should not be created
By("Check the workload is NOT created")
workloadIn := tempFoo.DeepCopy()
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: inName}, workloadIn)).Should(&util.NotFoundMatcher{})
// modify the trait to make it satisfy comp's dependency
outputTrait = tempFoo.DeepCopy()
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: outName}, outputTrait)).Should(Succeed())
err := unstructured.SetNestedField(outputTrait.Object, "test", "status", "key")
Expect(err).Should(BeNil())
Expect(k8sClient.Status().Update(ctx, outputTrait)).Should(Succeed())
Eventually(func() string {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: outName}, outputTrait)
data, _, _ := unstructured.NestedString(outputTrait.Object, "status", "key")
return data
}, 3*time.Second, time.Second).Should(Equal("test"))
By("Reconcile & check ac is satisfied")
Eventually(func() []v1alpha2.UnstaifiedDependency {
reconcileRetry(reconciler, reqDep)
acWithDep = v1alpha2.ApplicationConfiguration{}
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep); err != nil {
return []v1alpha2.UnstaifiedDependency{{Reason: err.Error()}}
}
return acWithDep.Status.Dependency.Unsatisfied
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile & check workload is created")
Eventually(func() error {
reconcileRetry(reconciler, reqDep)
// the workload is created now because its dependency is satisfied
workloadIn := tempFoo.DeepCopy()
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: inName}, workloadIn)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Delete the workload")
recreatedWL := tempFoo.DeepCopy()
recreatedWL.SetName(inName)
Expect(k8sClient.Delete(ctx, recreatedWL)).Should(Succeed())
outputTrait = tempFoo.DeepCopy()
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: inName}, outputTrait)).Should(util.NotFoundMatcher{})
By("Reconcile")
Expect(func() error { _, err := reconciler.Reconcile(req); return err }()).Should(BeNil())
time.Sleep(3 * time.Second)
By("Check workload is not re-created by reconciliation")
inputWorkload = tempFoo.DeepCopy()
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: inName}, inputWorkload)).Should(util.NotFoundMatcher{})
})
It("tests the situation where workload is not applied at the first because of unsatisfied dependency and revision specified",
func() {
componentHandler := &ComponentHandler{Client: k8sClient, RevisionLimit: 100, Logger: logging.NewLogrLogger(ctrl.Log.WithName("component-handler"))}
By("Enable ApplyOnceOnlyForce")
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
tempFoo := &unstructured.Unstructured{}
tempFoo.SetAPIVersion("example.com/v1")
tempFoo.SetKind("Foo")
tempFoo.SetNamespace(namespace)
inputWorkload := &unstructured.Unstructured{}
inputWorkload.SetAPIVersion("example.com/v1")
inputWorkload.SetKind("Foo")
inputWorkload.SetNamespace(namespace)
compInName := "comp-in-revision"
compIn := v1alpha2.Component{
ObjectMeta: metav1.ObjectMeta{
Name: compInName,
Namespace: namespace,
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{
Object: inputWorkload,
},
},
}
outName := "data-output"
outputTrait := tempFoo.DeepCopy()
outputTrait.SetName(outName)
acWithDepName := "ac-dep"
acWithDep := v1alpha2.ApplicationConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: acWithDepName,
Namespace: namespace,
},
Spec: v1alpha2.ApplicationConfigurationSpec{
Components: []v1alpha2.ApplicationConfigurationComponent{
{
RevisionName: compInName + "-v1",
DataInputs: []v1alpha2.DataInput{
{
ValueFrom: v1alpha2.DataInputValueFrom{
DataOutputName: "trait-output",
},
ToFieldPaths: []string{"spec.key"},
},
},
Traits: []v1alpha2.ComponentTrait{{
Trait: runtime.RawExtension{Object: outputTrait},
DataOutputs: []v1alpha2.DataOutput{{
Name: "trait-output",
FieldPath: "status.key",
}},
},
},
},
},
},
}
By("Create Component")
Expect(k8sClient.Create(ctx, &compIn)).Should(Succeed())
cmp := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, cmp)).Should(Succeed())
cmpV1 := cmp.DeepCopy()
By("component handler will automatically create controller revision")
Expect(func() bool {
_, ok := componentHandler.createControllerRevision(cmpV1, cmpV1)
return ok
}()).Should(BeTrue())
By("Creat appConfig & check successfully")
Expect(k8sClient.Create(ctx, &acWithDep)).Should(Succeed())
Eventually(func() error {
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile & check successfully")
reqDep := reconcile.Request{
NamespacedName: client.ObjectKey{Namespace: namespace, Name: acWithDepName},
}
Eventually(func() bool {
reconcileRetry(reconciler, reqDep)
acWithDep = v1alpha2.ApplicationConfiguration{}
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep); err != nil {
return false
}
return len(acWithDep.Status.Workloads) == 1
}, time.Second, 300*time.Millisecond).Should(BeTrue())
// because dependency is not satisfied so the workload should not be created
By("Check the workload is NOT created")
workloadIn := tempFoo.DeepCopy()
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName + "-v1"}, workloadIn)).Should(&util.NotFoundMatcher{})
// modify the trait to make it satisfy comp's dependency
outputTrait = tempFoo.DeepCopy()
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: outName}, outputTrait)).Should(Succeed())
err := unstructured.SetNestedField(outputTrait.Object, "test", "status", "key")
Expect(err).Should(BeNil())
Expect(k8sClient.Status().Update(ctx, outputTrait)).Should(Succeed())
Eventually(func() string {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: outName}, outputTrait)
data, _, _ := unstructured.NestedString(outputTrait.Object, "status", "key")
return data
}, 3*time.Second, time.Second).Should(Equal("test"))
By("Reconcile & check ac is satisfied")
Eventually(func() []v1alpha2.UnstaifiedDependency {
reconcileRetry(reconciler, reqDep)
acWithDep = v1alpha2.ApplicationConfiguration{}
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep); err != nil {
return []v1alpha2.UnstaifiedDependency{{Reason: err.Error()}}
}
return acWithDep.Status.Dependency.Unsatisfied
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile & check workload is created")
Eventually(func() error {
reconcileRetry(reconciler, reqDep)
// the workload should be created now because its dependency is satisfied
workloadIn := tempFoo.DeepCopy()
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, workloadIn)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Delete the workload")
recreatedWL := tempFoo.DeepCopy()
recreatedWL.SetName(compInName)
Expect(k8sClient.Delete(ctx, recreatedWL)).Should(Succeed())
inputWorkload2 := tempFoo.DeepCopy()
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, inputWorkload2)).Should(util.NotFoundMatcher{})
By("Reconcile")
Expect(func() error { _, err := reconciler.Reconcile(req); return err }()).Should(BeNil())
time.Sleep(3 * time.Second)
By("Check workload is not re-created by reconciliation")
inputWorkload = tempFoo.DeepCopy()
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, inputWorkload)).Should(util.NotFoundMatcher{})
})
It("should normally create workload/trait resources at fist time", func() {
By("Enable ApplyOnceOnlyForce")
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
component2 := v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1alpha2",
Kind: "Component",
},
ObjectMeta: metav1.ObjectMeta{
Name: "mycomp2",
Namespace: namespace,
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{
Object: &cw,
},
},
}
newFakeTrait := fakeTrait.DeepCopy()
newFakeTrait.SetName("mytrait2")
appConfig2 := v1alpha2.ApplicationConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "myac2",
Namespace: namespace,
},
Spec: v1alpha2.ApplicationConfigurationSpec{
Components: []v1alpha2.ApplicationConfigurationComponent{
{
ComponentName: "mycomp2",
Traits: []v1alpha2.ComponentTrait{
{Trait: runtime.RawExtension{Object: newFakeTrait}},
},
},
},
},
}
By("Create Component")
Expect(k8sClient.Create(ctx, &component2)).Should(Succeed())
time.Sleep(time.Second)
By("Creat appConfig & check successfully")
Expect(k8sClient.Create(ctx, &appConfig2)).Should(Succeed())
time.Sleep(time.Second)
By("Reconcile")
Expect(func() error {
_, err := reconciler.Reconcile(reconcile.Request{NamespacedName: types.NamespacedName{Name: "myac2", Namespace: namespace}})
return err
}()).Should(BeNil())
time.Sleep(2 * time.Second)
By("Get workload instance & Check workload spec")
cwObj := v1alpha2.ContainerizedWorkload{}
Eventually(func() error {
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: "mycomp2"}, &cwObj)
}, 5*time.Second, time.Second).Should(BeNil())
Expect(cwObj.Spec.Containers[0].Image).Should(Equal(image1))
By("Get trait instance & Check trait spec")
fooObj := &unstructured.Unstructured{}
fooObj.SetAPIVersion("example.com/v1")
fooObj.SetKind("Foo")
Eventually(func() error {
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: "mytrait2"}, fooObj)
}, 3*time.Second, time.Second).Should(BeNil())
fooObjV, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
Expect(fooObjV).Should(Equal(traitSpecValue1))
})
It("should not revert changes of workload/trait made by others", func() {
By("Enable ApplyOnceOnlyForce")
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
@@ -292,6 +669,8 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
By("Get workload instance & Check workload spec")
cwObj := v1alpha2.ContainerizedWorkload{}
Eventually(func() error {
By("Reconcile")
reconcileRetry(reconciler, req)
return k8sClient.Get(ctx, cwObjKey, &cwObj)
}, 5*time.Second, time.Second).Should(BeNil())
Expect(cwObj.Spec.Containers[0].Image).Should(Equal(image1))
@@ -393,8 +772,10 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
By("Get workload instance")
cwObj := v1alpha2.ContainerizedWorkload{}
Eventually(func() error {
By("Reconcile")
reconcileRetry(reconciler, req)
return k8sClient.Get(ctx, cwObjKey, &cwObj)
}, 3*time.Second, time.Second).Should(BeNil())
}, 5*time.Second, time.Second).Should(BeNil())
By("Get trait instance & Check trait spec")
fooObj := unstructured.Unstructured{}
@@ -426,7 +807,7 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
recreatedFooObj.SetKind("Foo")
Expect(k8sClient.Get(ctx, traitObjKey, &recreatedFooObj)).Should(util.NotFoundMatcher{})
By("Update Appconfig to trigger generation augment")
By("Update AppConfig to trigger generation updated")
unstructured.SetNestedField(fakeTrait.Object, "newvalue", "spec", "key")
appConfig = v1alpha2.ApplicationConfiguration{
ObjectMeta: metav1.ObjectMeta{
@@ -445,14 +826,24 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
},
}
Expect(k8sClient.Patch(ctx, &appConfig, client.Merge)).Should(Succeed())
time.Sleep(1 * time.Second)
By("Reconcile")
reconcileRetry(reconciler, req)
time.Sleep(3 * time.Second)
By("Check AppConfig is updated successfully")
updateAC := v1alpha2.ApplicationConfiguration{}
Eventually(func() int64 {
if err := k8sClient.Get(ctx, appConfigKey, &updateAC); err != nil {
return 0
}
return updateAC.GetGeneration()
}, 3*time.Second, time.Second).Should(Equal(int64(2)))
By("Check workload is re-created by reconciliation")
recreatedCwObj = v1alpha2.ContainerizedWorkload{}
Expect(k8sClient.Get(ctx, cwObjKey, &recreatedCwObj)).Should(Succeed())
By("Check workload was not created by reconciliation")
Eventually(func() error {
By("Reconcile")
reconcileRetry(reconciler, req)
recreatedCwObj = v1alpha2.ContainerizedWorkload{}
return k8sClient.Get(ctx, cwObjKey, &recreatedCwObj)
}, 5*time.Second, time.Second).Should(SatisfyAll(util.NotFoundMatcher{}))
By("Check trait is re-created by reconciliation")
recreatedFooObj = unstructured.Unstructured{}

View File

@@ -126,9 +126,8 @@ func TestApplyWorkloads(t *testing.T) {
}
type args struct {
ctx context.Context
ws []v1alpha2.WorkloadStatus
w []Workload
ws []v1alpha2.WorkloadStatus
w []Workload
}
cases := map[string]struct {
@@ -330,7 +329,7 @@ func TestApplyWorkloads(t *testing.T) {
t.Run(name, func(t *testing.T) {
mapper := mock.NewMockDiscoveryMapper()
w := workloads{applicator: tc.applicator, rawClient: tc.rawClient, dm: mapper}
err := w.Apply(tc.args.ctx, tc.args.ws, tc.args.w)
err := w.Apply(context.TODO(), tc.args.ws, tc.args.w)
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nw.Apply(...): -want error, +got error:\n%s", tc.reason, diff)

View File

@@ -149,7 +149,8 @@ var _ = Describe("Test apply changes to trait", func() {
APIVersion: "TraitDefinition",
},
ObjectMeta: metav1.ObjectMeta{
Name: "bars.example.com",
Name: "bars.example.com",
Namespace: "vela-system",
},
Spec: v1alpha2.TraitDefinitionSpec{
Reference: v1alpha2.DefinitionReference{
@@ -236,31 +237,26 @@ spec:
return appConfig.GetGeneration()
}, time.Second, 300*time.Millisecond).Should(Equal(int64(2)))
By("Reconcile")
reconcileRetry(reconciler, req)
Eventually(func() string {
By("Reconcile & check updated trait")
var traitObj unstructured.Unstructured
Eventually(func() int64 {
reconcileRetry(reconciler, req)
if err := k8sClient.Get(ctx, appConfigKey, &appConfig); err != nil {
return ""
return 0
}
if appConfig.Status.Workloads == nil {
reconcileRetry(reconciler, req)
return ""
return 0
}
return appConfig.Status.Workloads[0].Traits[0].Reference.Name
}, 3*time.Second, time.Second).ShouldNot(BeEmpty())
By("Get changed trait object")
traitName := appConfig.Status.Workloads[0].Traits[0].Reference.Name
var traitObj unstructured.Unstructured
traitObj.SetAPIVersion("example.com/v1")
traitObj.SetKind("Bar")
Eventually(func() int64 {
traitName := appConfig.Status.Workloads[0].Traits[0].Reference.Name
traitObj.SetAPIVersion("example.com/v1")
traitObj.SetKind("Bar")
if err := k8sClient.Get(ctx,
client.ObjectKey{Namespace: namespace, Name: traitName}, &traitObj); err != nil {
return 0
}
return traitObj.GetGeneration()
}, 3*time.Second, time.Second).Should(Equal(int64(2)))
}, 5*time.Second, time.Second).Should(Equal(int64(2)))
By("Check labels are removed")
_, found, _ := unstructured.NestedString(traitObj.UnstructuredContent(), "metadata", "labels", "test.label")
@@ -355,13 +351,19 @@ spec:
}
return appConfig.GetGeneration()
}, time.Second, 300*time.Millisecond).Should(Equal(int64(2)))
By("Reconcile")
reconcileRetry(reconciler, req)
changedTrait = unstructured.Unstructured{}
changedTrait.SetAPIVersion("example.com/v1")
changedTrait.SetKind("Bar")
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: traitName}, &changedTrait)).Should(Succeed())
Eventually(func() int64 {
By("Reconcile")
reconcileRetry(reconciler, req)
changedTrait = unstructured.Unstructured{}
changedTrait.SetAPIVersion("example.com/v1")
changedTrait.SetKind("Bar")
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: traitName}, &changedTrait); err != nil {
return 0
}
return changedTrait.GetGeneration()
}, 5*time.Second, time.Second).Should(Equal(int64(3)))
By("Check AppConfig's change works")
// changed a field
v, _, _ = unstructured.NestedString(changedTrait.UnstructuredContent(), "spec", "valueChanged")

View File

@@ -81,7 +81,7 @@ func (c *ComponentHandler) Generic(_ event.GenericEvent, _ workqueue.RateLimitin
func isMatch(appConfigs *v1alpha2.ApplicationConfigurationList, compName string) (bool, types.NamespacedName) {
for _, app := range appConfigs.Items {
for _, comp := range app.Spec.Components {
if comp.ComponentName == compName {
if comp.ComponentName == compName || (strings.HasPrefix(comp.RevisionName, compName+"-")) {
return true, types.NamespacedName{Namespace: app.Namespace, Name: app.Name}
}
}
@@ -140,6 +140,10 @@ func newTrue() *bool {
func (c *ComponentHandler) createControllerRevision(mt metav1.Object, obj runtime.Object) ([]reconcile.Request, bool) {
curComp := obj.(*v1alpha2.Component)
comp := curComp.DeepCopy()
// No generation changed, will not create revision
if comp.Generation == comp.Status.ObservedGeneration {
return nil, false
}
diff, curRevision := c.IsRevisionDiff(mt, comp)
if !diff {
// No difference, no need to create new revision.

View File

@@ -18,50 +18,68 @@ package applicationconfiguration
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
)
func TestCustomRevisionHook(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req RevisionHookRequest
data, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(400)
return
}
err = json.Unmarshal(data, &req)
if err != nil {
w.WriteHeader(401)
return
}
if len(req.RelatedApps) != 1 {
w.WriteHeader(400)
w.Write([]byte("we should have only one relatedApps"))
return
}
if req.Comp.Annotations == nil {
req.Comp.Annotations = make(map[string]string)
var RevisionHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req RevisionHookRequest
data, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(400)
return
}
err = json.Unmarshal(data, &req)
if err != nil {
w.WriteHeader(401)
return
}
fmt.Println("got request from", req.Comp.Name)
if len(req.RelatedApps) != 1 {
var abc []string
for _, v := range req.RelatedApps {
abc = append(abc, v.Name)
}
// we can add a check here for real world handler
fmt.Printf("we should have only one relatedApps, but now %d: %s\n", len(req.RelatedApps), strings.Join(abc, ", "))
}
if req.Comp.Annotations == nil {
req.Comp.Annotations = make(map[string]string)
}
if len(req.RelatedApps) > 0 {
req.Comp.Annotations["app-name"] = req.RelatedApps[0].Name
req.Comp.Annotations["app-namespace"] = req.RelatedApps[0].Namespace
}
a := &unstructured.Unstructured{}
err = json.Unmarshal(req.Comp.Spec.Workload.Raw, a)
fmt.Println("XX:", err)
a.SetAnnotations(map[string]string{"time": time.Now().Format(time.RFC3339Nano)})
data, _ = json.Marshal(a)
req.Comp.Spec.Workload.Raw = data
newdata, err := json.Marshal(req.Comp)
if err != nil {
w.WriteHeader(500)
return
}
w.WriteHeader(200)
w.Write(newdata)
})
newdata, err := json.Marshal(req.Comp)
if err != nil {
w.WriteHeader(500)
return
}
w.WriteHeader(200)
w.Write(newdata)
}))
func TestCustomRevisionHook(t *testing.T) {
srv := httptest.NewServer(RevisionHandler)
defer srv.Close()
compHandler := ComponentHandler{
CustomRevisionHookURL: srv.URL,
@@ -71,7 +89,4 @@ func TestCustomRevisionHook(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "app1", comp.Annotations["app-name"])
assert.Equal(t, "default1", comp.Annotations["app-namespace"])
err = compHandler.customComponentRevisionHook([]reconcile.Request{{NamespacedName: types.NamespacedName{Name: "app1", Namespace: "default1"}}, {NamespacedName: types.NamespacedName{Name: "app2", Namespace: "default2"}}}, comp)
assert.Equal(t, err.Error(), "httpcode(400) err: we should have only one relatedApps")
}

View File

@@ -138,7 +138,7 @@ func TestComponentHandler(t *testing.T) {
// check component's status saved in corresponding controllerRevision
assert.Equal(t, gotComp.Status.LatestRevision.Name, revisions.Items[0].Name)
assert.Equal(t, gotComp.Status.LatestRevision.Revision, revisions.Items[0].Revision)
// check component's status ObservedGeneration
// check component's status AppliedGeneration
assert.Equal(t, gotComp.Status.ObservedGeneration, comp.Generation)
q.Done(item)
// ============ Test Create Event End ===================

View File

@@ -80,8 +80,23 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
AfterEach(func() {
logf.Log.Info("Clean up resources")
// delete the namespace with all its resources
Expect(k8sClient.Delete(ctx, in)).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{}))
Expect(k8sClient.Delete(ctx, out)).Should(BeNil())
ac := &v1alpha2.ApplicationConfiguration{}
Expect(k8sClient.DeleteAllOf(ctx, ac, client.InNamespace(namespace))).Should(Succeed())
cm := &corev1.ConfigMap{}
Expect(k8sClient.DeleteAllOf(ctx, cm, client.InNamespace(namespace))).Should(Succeed())
foo := &unstructured.Unstructured{}
foo.SetAPIVersion("example.com/v1")
foo.SetKind("Foo")
Expect(k8sClient.DeleteAllOf(ctx, foo, client.InNamespace(namespace))).Should(Succeed())
Eventually(func() bool {
l := &unstructured.UnstructuredList{}
l.SetAPIVersion("example.com/v1")
l.SetKind("Foo")
if err := k8sClient.List(ctx, l, client.InNamespace(namespace)); err != nil {
return false
}
return len(l.Items) == 0
}, 3*time.Second, time.Second).Should(BeTrue())
})
// common function for verification
@@ -114,11 +129,11 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
Eventually(func() error {
err := k8sClient.Get(ctx, outFooKey, outFoo)
if err != nil {
// Try 3 (= 1s/300ms) times
// Try 3 (= 3s/1s) times
reconciler.Reconcile(req)
}
return err
}, time.Second, 300*time.Millisecond).Should(BeNil())
}, 3*time.Second, time.Second).Should(BeNil())
By("Reconcile")
reconcileRetry(reconciler, req)
@@ -159,7 +174,7 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
k8sClient.Get(ctx, outFooKey, outFoo)
data, _, _ := unstructured.NestedString(outFoo.Object, "status", "key")
return data
}, time.Second, 300*time.Millisecond).Should(Equal("test"))
}, 3*time.Second, time.Second).Should(Equal("test"))
By("Reconcile")
reconcileRetry(reconciler, req)
@@ -175,7 +190,7 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
reconciler.Reconcile(req)
k8sClient.Get(ctx, appconfigKey, appconfig)
return appconfig.Status.Dependency.Unsatisfied
}, 2*time.Second, 300*time.Millisecond).Should(BeNil())
}, 3*time.Second, time.Second).Should(BeNil())
}
It("trait depends on another trait", func() {
@@ -372,7 +387,7 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
Expect(k8sClient.Create(ctx, &appConfig)).Should(Succeed())
Eventually(func() error {
return k8sClient.Get(ctx, appconfigKey, &appConfig)
}, time.Second, 300*time.Millisecond).Should(BeNil())
}, 3*time.Second, time.Second).Should(BeNil())
By("Reconcile")
reconcileRetry(reconciler, req)
@@ -399,11 +414,11 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
Eventually(func() error {
err := k8sClient.Get(ctx, outFooKey, outFoo)
if err != nil {
// Try 3 (= 1s/300ms) times
// Try 3 (= 3s/1s) times
reconciler.Reconcile(req)
}
return err
}, time.Second, 300*time.Millisecond).Should(BeNil())
}, 3*time.Second, time.Second).Should(BeNil())
err := unstructured.SetNestedField(outFoo.Object, "test", "status", "key")
Expect(err).Should(BeNil())
@@ -423,11 +438,11 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
err = k8sClient.Get(ctx, appconfigKey, tempApp)
tempApp.DeepCopyInto(newAppConfig)
if err != nil || tempApp.Status.Dependency.Unsatisfied != nil {
// Try 3 (= 1s/300ms) times
// Try 3 (= 3s/1s) times
reconciler.Reconcile(req)
}
return tempApp.Status.Dependency.Unsatisfied
}(), time.Second, 300*time.Millisecond).Should(BeNil())
}(), 3*time.Second, time.Second).Should(BeNil())
By("Checking that resource which accepts data is created now")
logf.Log.Info("Checking on resource that inputs data", "Key", inFooKey)
@@ -436,11 +451,11 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
Eventually(func() error {
err := k8sClient.Get(ctx, outFooKey, outFoo)
if err != nil {
// Try 3 (= 1s/300ms) times
// Try 3 (= 3s/1s) times
reconciler.Reconcile(req)
}
return err
}, time.Second, 300*time.Millisecond).Should(BeNil())
}, 3*time.Second, time.Second).Should(BeNil())
err = unstructured.SetNestedField(outFoo.Object, "test", "status", "key")
Expect(err).Should(BeNil())
err = unstructured.SetNestedField(outFoo.Object, "hash-v1", "status", "app-hash")
@@ -453,7 +468,7 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
}
s, _, _ := unstructured.NestedString(outFoo.Object, "status", "key")
return s == "test"
}, time.Second, 300*time.Millisecond).Should(BeTrue())
}, 3*time.Second, time.Second).Should(BeTrue())
newAppConfig.Labels["app-hash"] = "hash-v2"
By("Update newAppConfig & check successfully")
@@ -464,10 +479,7 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
return false
}
return newAppConfig.Labels["app-hash"] == "hash-v2"
}, time.Second, 300*time.Millisecond).Should(BeTrue())
By("Reconcile")
reconcileRetry(reconciler, req)
}, 3*time.Second, time.Second).Should(BeTrue())
By("Verify the appconfig's dependency should be unsatisfied, because requirementCondition valueFrom not match")
depStatus := v1alpha2.DependencyStatus{
@@ -492,9 +504,11 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
}}}},
}
Eventually(func() v1alpha2.DependencyStatus {
By("Reconcile")
reconcileRetry(reconciler, req)
k8sClient.Get(ctx, appconfigKey, newAppConfig)
return newAppConfig.Status.Dependency
}, time.Second, 300*time.Millisecond).Should(Equal(depStatus))
}, 3*time.Second, time.Second).Should(Equal(depStatus))
By("Update trait resource to meet the requirement")
Expect(k8sClient.Get(ctx, outFooKey, outFoo)).Should(BeNil()) // Get the latest before update
@@ -508,7 +522,7 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
}
s, _, _ := unstructured.NestedString(outFoo.Object, "status", "key")
return s == "test-new"
}, time.Second, 300*time.Millisecond).Should(BeTrue())
}, 3*time.Second, time.Second).Should(BeTrue())
By("Reconcile")
reconcileRetry(reconciler, req)
@@ -519,11 +533,11 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
tempAppConfig := &v1alpha2.ApplicationConfiguration{}
err := k8sClient.Get(ctx, appconfigKey, tempAppConfig)
if err != nil || tempAppConfig.Status.Dependency.Unsatisfied != nil {
// Try 3 (= 1s/300ms) times
// Try 3 (= 3s/1s) times
reconciler.Reconcile(req)
}
return tempAppConfig.Status.Dependency.Unsatisfied
}(), time.Second, 300*time.Millisecond).Should(BeNil())
}(), 3*time.Second, time.Second).Should(BeNil())
By("Checking that resource which accepts data is updated")
Expect(func() string {
@@ -625,6 +639,9 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
Name: appConfigName,
Namespace: namespace,
}
Eventually(func() error {
return k8sClient.Get(ctx, appconfigKey, &v1alpha2.ApplicationConfiguration{})
}, 3*time.Second, time.Second).Should(Succeed())
By("Reconcile")
req := reconcile.Request{NamespacedName: appconfigKey}
reconcileRetry(reconciler, req)
@@ -641,7 +658,7 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
reconciler.Reconcile(req)
}
return err
}, time.Second, 300*time.Millisecond).Should(BeNil())
}, 3*time.Second, time.Second).Should(BeNil())
By("Get reconciled AppConfig the first time")
appconfig := &v1alpha2.ApplicationConfiguration{}
@@ -669,7 +686,7 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
k8sClient.Get(ctx, outFooKey, outFoo)
data, _, _ := unstructured.NestedSlice(outFoo.Object, "status", "complex2")
return data
}, time.Second, 300*time.Millisecond).Should(BeEquivalentTo(complex2))
}, 3*time.Second, time.Second).Should(BeEquivalentTo(complex2))
By("Reconcile")
reconcileRetry(reconciler, req)
@@ -680,7 +697,7 @@ var _ = Describe("Resource Dependency in an ApplicationConfiguration", func() {
reconciler.Reconcile(req)
k8sClient.Get(ctx, appconfigKey, appconfig)
return appconfig.Status.Dependency.Unsatisfied
}, 2*time.Second, 300*time.Millisecond).Should(BeNil())
}, 2*3*time.Second, time.Second).Should(BeNil())
// Verification after satisfying dependency
By("Checking that resource which accepts data is created now")
inFooKey := client.ObjectKey{

View File

@@ -39,7 +39,6 @@ import (
core "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/mock"
"github.com/oam-dev/kubevela/pkg/oam/util"
@@ -99,8 +98,7 @@ func TestRenderComponents(t *testing.T) {
trait ResourceRenderer
}
type args struct {
ctx context.Context
ac *v1alpha2.ApplicationConfiguration
ac *v1alpha2.ApplicationConfiguration
}
type want struct {
w []Workload
@@ -525,7 +523,7 @@ func TestRenderComponents(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := &components{tc.fields.client, mock.NewMockDiscoveryMapper(), tc.fields.params, tc.fields.workload, tc.fields.trait}
got, _, err := r.Render(tc.args.ctx, tc.args.ac)
got, _, err := r.Render(context.TODO(), tc.args.ac)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nr.Render(...): -want error, +got error:\n%s\n", tc.reason, diff)
}
@@ -813,8 +811,7 @@ func TestRenderTraitWithoutMetadataName(t *testing.T) {
trait ResourceRenderer
}
type args struct {
ctx context.Context
ac *v1alpha2.ApplicationConfiguration
ac *v1alpha2.ApplicationConfiguration
}
type want struct {
w []Workload
@@ -871,7 +868,7 @@ func TestRenderTraitWithoutMetadataName(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := &components{tc.fields.client, mock.NewMockDiscoveryMapper(), tc.fields.params, tc.fields.workload, tc.fields.trait}
got, _, _ := r.Render(tc.args.ctx, tc.args.ac)
got, _, _ := r.Render(context.TODO(), tc.args.ac)
if len(got) == 0 || len(got[0].Traits) == 0 || got[0].Traits[0].Object.GetName() != util.GenTraitName(componentName, ac.Spec.Components[0].Traits[0].DeepCopy(), "") {
t.Errorf("\n%s\nr.Render(...): -want error, +got error:\n%s\n", tc.reason, "Trait name is NOT "+
"automatically set.")

View File

@@ -19,18 +19,21 @@ package applicationconfiguration
import (
"context"
"fmt"
"net/http/httptest"
"strconv"
"time"
v1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/crossplane/crossplane-runtime/pkg/logging"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -200,11 +203,10 @@ var _ = Describe("Test ApplicationConfiguration Component Revision Enabled trait
return k8sClient.Get(ctx, appConfigKey, &appConfig)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile")
reconcileRetry(reconciler, req)
By("Check workload created successfully")
Eventually(func() error {
By("Reconcile")
reconcileRetry(reconciler, req)
var workloadKey = client.ObjectKey{Namespace: namespace, Name: compName + "-v1"}
return k8sClient.Get(ctx, workloadKey, &wr)
}, 3*time.Second, 300*time.Millisecond).Should(BeNil())
@@ -295,11 +297,11 @@ var _ = Describe("Test ApplicationConfiguration Component Revision Enabled trait
Eventually(func() error {
return k8sClient.Get(ctx, appConfigKey, &appConfig)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile for new revision")
reconcileRetry(reconciler, req)
By("Check new revision workload created successfully")
Eventually(func() error {
By("Reconcile for new revision")
reconcileRetry(reconciler, req)
var workloadKey = client.ObjectKey{Namespace: namespace, Name: compName + "-v2"}
return k8sClient.Get(ctx, workloadKey, &wr)
}, time.Second, 300*time.Millisecond).Should(BeNil())
@@ -341,3 +343,824 @@ var _ = Describe("Test ApplicationConfiguration Component Revision Enabled trait
})
})
var _ = Describe("Test Component Revision Enabled with custom component revision hook", func() {
const (
namespace = "revision-enable-test2"
compName = "revision-test-comp2"
)
var (
ctx = context.Background()
component v1alpha2.Component
ns = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
}
)
BeforeEach(func() {})
AfterEach(func() {
// delete the namespace with all its resources
Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).
Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{}))
})
It("custom component change revision lead to revision difference, it should not loop infinitely create", func() {
srv := httptest.NewServer(RevisionHandler)
defer srv.Close()
customComponentHandler := &ComponentHandler{Client: k8sClient, RevisionLimit: 100, Logger: logging.NewLogrLogger(ctrl.Log.WithName("component-handler")), CustomRevisionHookURL: srv.URL}
getDeploy := func(image string) *v1.Deployment {
return &v1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
},
Spec: v1.DeploymentSpec{
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{
"app": compName,
}},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"app": compName,
}},
Spec: corev1.PodSpec{Containers: []corev1.Container{{
Name: "wordpress",
Image: image,
Ports: []corev1.ContainerPort{
{
Name: "wordpress",
ContainerPort: 80,
},
},
},
}}},
},
}
}
component = v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1alpha2",
Kind: "Component",
},
ObjectMeta: metav1.ObjectMeta{
Name: compName,
Namespace: namespace,
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{
Object: getDeploy("wordpress:4.6.1-apache"),
},
},
}
By("Create namespace")
Eventually(
func() error {
return k8sClient.Create(ctx, &ns)
},
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
By("Create Component")
Expect(k8sClient.Create(ctx, &component)).Should(Succeed())
By("component handler will automatically create controller revision")
Expect(func() bool {
_, ok := customComponentHandler.createControllerRevision(component.DeepCopy(), component.DeepCopy())
return ok
}()).Should(BeTrue())
By("it should not create again for the same generation component")
cmpV1 := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV1)).Should(Succeed())
Expect(func() bool {
_, ok := customComponentHandler.createControllerRevision(cmpV1, cmpV1)
return ok
}()).Should(BeFalse())
var crList v1.ControllerRevisionList
By("Check controller revision created successfully")
Eventually(func() error {
labels := &metav1.LabelSelector{
MatchLabels: map[string]string{
ControllerRevisionComponentLabel: compName,
},
}
selector, err := metav1.LabelSelectorAsSelector(labels)
if err != nil {
return err
}
err = k8sClient.List(ctx, &crList, &client.ListOptions{
LabelSelector: selector,
})
if err != nil {
return err
}
if len(crList.Items) != 1 {
return fmt.Errorf("want only 1 revision created but got %d", len(crList.Items))
}
return nil
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("===================================== Start to Update =========================================")
cmpV2 := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV2)).Should(Succeed())
cmpV2.Spec.Workload = runtime.RawExtension{
Object: getDeploy("wordpress:v2"),
}
By("Update Component")
Expect(k8sClient.Update(ctx, cmpV2)).Should(Succeed())
By("component handler will automatically create a ne controller revision")
Expect(func() bool { _, ok := componentHandler.createControllerRevision(cmpV2, cmpV2); return ok }()).Should(BeTrue())
cmpV3 := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV3)).Should(Succeed())
Expect(func() bool { _, ok := componentHandler.createControllerRevision(cmpV3, cmpV3); return ok }()).Should(BeFalse())
By("Check controller revision created successfully")
Eventually(func() error {
labels := &metav1.LabelSelector{
MatchLabels: map[string]string{
ControllerRevisionComponentLabel: compName,
},
}
selector, err := metav1.LabelSelectorAsSelector(labels)
if err != nil {
return err
}
err = k8sClient.List(ctx, &crList, &client.ListOptions{
LabelSelector: selector,
})
if err != nil {
return err
}
if len(crList.Items) != 2 {
return fmt.Errorf("there should be exactly 2 revision created but got %d", len(crList.Items))
}
return nil
}, time.Second, 300*time.Millisecond).Should(BeNil())
})
})
var _ = Describe("Component Revision Enabled with apply once only force", func() {
const (
namespace = "revision-and-apply-once-force"
appName = "revision-apply-once"
compName = "revision-apply-once-comp"
)
var (
ctx = context.Background()
wr v1.Deployment
component v1alpha2.Component
appConfig v1alpha2.ApplicationConfiguration
appConfigKey = client.ObjectKey{
Name: appName,
Namespace: namespace,
}
req = reconcile.Request{NamespacedName: appConfigKey}
ns = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
}
)
BeforeEach(func() {})
AfterEach(func() {
// delete the namespace with all its resources
Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).
Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{}))
})
It("revision enabled should create workload with revisionName and work upgrade with new revision successfully", func() {
getDeploy := func(image string) *v1.Deployment {
return &v1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
},
Spec: v1.DeploymentSpec{
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{
"app": compName,
}},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"app": compName,
}},
Spec: corev1.PodSpec{Containers: []corev1.Container{{
Name: "wordpress",
Image: image,
Ports: []corev1.ContainerPort{
{
Name: "wordpress",
ContainerPort: 80,
},
},
},
}}},
},
}
}
component = v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1alpha2",
Kind: "Component",
},
ObjectMeta: metav1.ObjectMeta{
Name: compName,
Namespace: namespace,
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{
Object: getDeploy("wordpress:4.6.1-apache"),
},
},
}
appConfig = v1alpha2.ApplicationConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: namespace,
},
}
By("Create namespace")
Eventually(
func() error {
return k8sClient.Create(ctx, &ns)
},
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
By("Create Component")
Expect(k8sClient.Create(ctx, &component)).Should(Succeed())
cmpV1 := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV1)).Should(Succeed())
By("component handler will automatically create controller revision")
Expect(func() bool {
_, ok := componentHandler.createControllerRevision(cmpV1, cmpV1)
return ok
}()).Should(BeTrue())
var crList v1.ControllerRevisionList
By("Check controller revision created successfully")
Eventually(func() error {
labels := &metav1.LabelSelector{
MatchLabels: map[string]string{
ControllerRevisionComponentLabel: compName,
},
}
selector, err := metav1.LabelSelectorAsSelector(labels)
if err != nil {
return err
}
err = k8sClient.List(ctx, &crList, &client.ListOptions{
LabelSelector: selector,
})
if err != nil {
return err
}
if len(crList.Items) != 1 {
return fmt.Errorf("want only 1 revision created but got %d", len(crList.Items))
}
return nil
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Create an ApplicationConfiguration")
appConfig = v1alpha2.ApplicationConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: namespace,
},
Spec: v1alpha2.ApplicationConfigurationSpec{Components: []v1alpha2.ApplicationConfigurationComponent{
{
RevisionName: compName + "-v1",
Traits: []v1alpha2.ComponentTrait{
{
Trait: runtime.RawExtension{Object: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "example.com/v1",
"kind": "Foo",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"trait.oam.dev/type": "rollout-revision",
},
},
"spec": map[string]interface{}{
"key": "test1",
},
}}},
},
},
},
}},
}
By("Creat appConfig & check successfully")
Expect(k8sClient.Create(ctx, &appConfig)).Should(Succeed())
Eventually(func() error {
return k8sClient.Get(ctx, appConfigKey, &appConfig)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile")
reconciler.applyOnceOnlyMode = "force"
reconcileRetry(reconciler, req)
By("Check workload created successfully")
var workloadKey1 = client.ObjectKey{Namespace: namespace, Name: compName + "-v1"}
Eventually(func() error {
reconcileRetry(reconciler, req)
return k8sClient.Get(ctx, workloadKey1, &wr)
}, 3*time.Second, 300*time.Millisecond).Should(BeNil())
By("Check workload should only have 1 generation")
Expect(wr.GetGeneration()).Should(BeEquivalentTo(1))
By("Delete the workload")
Expect(k8sClient.Delete(ctx, &wr)).Should(BeNil())
By("Check reconcile again and no error will happen")
reconcileRetry(reconciler, req)
By("Check workload will not created after reconcile because apply once force enabled")
Expect(k8sClient.Get(ctx, workloadKey1, &wr)).Should(SatisfyAll(util.NotFoundMatcher{}))
Expect(k8sClient.Get(ctx, appConfigKey, &appConfig)).Should(BeNil())
By("update the trait of ac")
appConfig.Spec.Components[0].Traits[0].Trait = runtime.RawExtension{Object: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "example.com/v1",
"kind": "Foo",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"trait.oam.dev/type": "rollout-revision",
},
},
"spec": map[string]interface{}{
"key": "test2",
},
}}}
Expect(k8sClient.Update(ctx, &appConfig)).Should(Succeed())
By("Reconcile and Check appconfig condition should not have error")
reconcileRetry(reconciler, req)
Eventually(func() string {
By("Reconcile again and should not have error")
reconcileRetry(reconciler, req)
err := k8sClient.Get(ctx, appConfigKey, &appConfig)
if err != nil {
return err.Error()
}
if len(appConfig.Status.Conditions) != 1 {
return "condition len should be 1 but now is " + strconv.Itoa(len(appConfig.Status.Conditions))
}
return string(appConfig.Status.Conditions[0].Reason)
}, 3*time.Second, 300*time.Millisecond).Should(BeEquivalentTo("ReconcileSuccess"))
time.Sleep(time.Second)
By("Check workload will not created even AC changed because apply once force working")
Expect(k8sClient.Get(ctx, workloadKey1, &wr)).Should(SatisfyAll(util.NotFoundMatcher{}))
By("Check the trait was updated as expected")
var tr unstructured.Unstructured
Eventually(func() error {
tr.SetAPIVersion("example.com/v1")
tr.SetKind("Foo")
var traitKey = client.ObjectKey{Namespace: namespace, Name: appConfig.Status.Workloads[0].Traits[0].Reference.Name}
return k8sClient.Get(ctx, traitKey, &tr)
}, time.Second, 300*time.Millisecond).Should(BeNil())
Expect(tr.Object["spec"]).Should(BeEquivalentTo(map[string]interface{}{"key": "test2"}))
By("===================================== Start to Upgrade revision of component =========================================")
cmpV2 := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV2)).Should(Succeed())
cmpV2.Spec.Workload = runtime.RawExtension{
Object: getDeploy("wordpress:v2"),
}
By("Update Component")
Expect(k8sClient.Update(ctx, cmpV2)).Should(Succeed())
By("component handler will automatically create a ne controller revision")
Expect(func() bool { _, ok := componentHandler.createControllerRevision(cmpV2, cmpV2); return ok }()).Should(BeTrue())
By("Check controller revision created successfully")
Eventually(func() error {
labels := &metav1.LabelSelector{
MatchLabels: map[string]string{
ControllerRevisionComponentLabel: compName,
},
}
selector, err := metav1.LabelSelectorAsSelector(labels)
if err != nil {
return err
}
err = k8sClient.List(ctx, &crList, &client.ListOptions{
LabelSelector: selector,
})
if err != nil {
return err
}
if len(crList.Items) != 2 {
return fmt.Errorf("there should be exactly 2 revision created but got %d", len(crList.Items))
}
return nil
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Update appConfig & check successfully")
appConfig.Spec.Components[0].RevisionName = compName + "-v2"
appConfig.Spec.Components[0].Traits[0].Trait = runtime.RawExtension{Object: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "example.com/v1",
"kind": "Foo",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"trait.oam.dev/type": "rollout-revision",
},
},
"spec": map[string]interface{}{
"key": "test3",
},
}}}
Expect(k8sClient.Update(ctx, &appConfig)).Should(Succeed())
Eventually(func() error {
return k8sClient.Get(ctx, appConfigKey, &appConfig)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile for new revision")
reconcileRetry(reconciler, req)
By("Check new revision workload created successfully")
Eventually(func() error {
reconcileRetry(reconciler, req)
var workloadKey = client.ObjectKey{Namespace: namespace, Name: compName + "-v2"}
return k8sClient.Get(ctx, workloadKey, &wr)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Check the new workload should only have 1 generation")
Expect(wr.GetGeneration()).Should(BeEquivalentTo(1))
Expect(wr.Spec.Template.Spec.Containers[0].Image).Should(BeEquivalentTo("wordpress:v2"))
By("Check the new workload should only have 1 generation")
Expect(wr.GetGeneration()).Should(BeEquivalentTo(1))
By("Check reconcile again and no error will happen")
reconcileRetry(reconciler, req)
By("Check appconfig condition should not have error")
Eventually(func() string {
By("Once more Reconcile and should not have error")
reconcileRetry(reconciler, req)
err := k8sClient.Get(ctx, appConfigKey, &appConfig)
if err != nil {
return err.Error()
}
if len(appConfig.Status.Conditions) != 1 {
return "condition len should be 1 but now is " + strconv.Itoa(len(appConfig.Status.Conditions))
}
return string(appConfig.Status.Conditions[0].Reason)
}, 3*time.Second, 300*time.Millisecond).Should(BeEquivalentTo("ReconcileSuccess"))
By("Check trait was updated as expected")
Eventually(func() error {
tr.SetAPIVersion("example.com/v1")
tr.SetKind("Foo")
var traitKey = client.ObjectKey{Namespace: namespace, Name: appConfig.Status.Workloads[0].Traits[0].Reference.Name}
return k8sClient.Get(ctx, traitKey, &tr)
}, time.Second, 300*time.Millisecond).Should(BeNil())
Expect(tr.Object["spec"]).Should(BeEquivalentTo(map[string]interface{}{"key": "test3"}))
reconciler.applyOnceOnlyMode = "off"
})
})
var _ = Describe("Component Revision Enabled with workloadName set and apply once only force", func() {
const (
namespace = "revision-and-workload-name-specified"
appName = "revision-apply-once2"
compName = "revision-apply-once-comp2"
specifiedNameBase = "specified-name-base"
specifiedNameV1 = "specified-name-v1"
specifiedNameV2 = "specified-name-v2"
)
var (
ctx = context.Background()
wr v1.Deployment
component v1alpha2.Component
appConfig v1alpha2.ApplicationConfiguration
appConfigKey = client.ObjectKey{
Name: appName,
Namespace: namespace,
}
req = reconcile.Request{NamespacedName: appConfigKey}
ns = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
}
)
BeforeEach(func() {})
AfterEach(func() {
// delete the namespace with all its resources
Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).
Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{}))
})
It("revision enabled should create workload with specified name protect delete with replicas larger than 0", func() {
getDeploy := func(image, name string) *v1.Deployment {
return &v1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: name,
},
Spec: v1.DeploymentSpec{
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{
"app": compName,
}},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"app": compName,
}},
Spec: corev1.PodSpec{Containers: []corev1.Container{{
Name: "wordpress",
Image: image,
Ports: []corev1.ContainerPort{
{
Name: "wordpress",
ContainerPort: 80,
},
},
},
}}},
},
}
}
component = v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1alpha2",
Kind: "Component",
},
ObjectMeta: metav1.ObjectMeta{
Name: compName,
Namespace: namespace,
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{
Object: getDeploy("wordpress:4.6.1-apache", specifiedNameBase),
},
},
}
appConfig = v1alpha2.ApplicationConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: namespace,
},
}
By("Create namespace")
Eventually(
func() error {
return k8sClient.Create(ctx, &ns)
},
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
By("Create Component")
Expect(k8sClient.Create(ctx, &component)).Should(Succeed())
cmpV1 := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV1)).Should(Succeed())
By("component handler will automatically create controller revision")
Expect(func() bool {
_, ok := componentHandler.createControllerRevision(cmpV1, cmpV1)
return ok
}()).Should(BeTrue())
var crList v1.ControllerRevisionList
By("Check controller revision created successfully")
Eventually(func() error {
labels := &metav1.LabelSelector{
MatchLabels: map[string]string{
ControllerRevisionComponentLabel: compName,
},
}
selector, err := metav1.LabelSelectorAsSelector(labels)
if err != nil {
return err
}
err = k8sClient.List(ctx, &crList, &client.ListOptions{
LabelSelector: selector,
})
if err != nil {
return err
}
if len(crList.Items) != 1 {
return fmt.Errorf("want only 1 revision created but got %d", len(crList.Items))
}
return nil
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Create an ApplicationConfiguration")
appConfig = v1alpha2.ApplicationConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: namespace,
},
Spec: v1alpha2.ApplicationConfigurationSpec{Components: []v1alpha2.ApplicationConfigurationComponent{
{
RevisionName: compName + "-v1",
},
}},
}
By("Creat appConfig & check successfully")
Expect(k8sClient.Create(ctx, &appConfig)).Should(Succeed())
Eventually(func() error {
return k8sClient.Get(ctx, appConfigKey, &appConfig)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile")
reconciler.applyOnceOnlyMode = "force"
reconcileRetry(reconciler, req)
By("Check workload created successfully")
var workloadKey1 = client.ObjectKey{Namespace: namespace, Name: specifiedNameBase}
Eventually(func() error {
reconcileRetry(reconciler, req)
return k8sClient.Get(ctx, workloadKey1, &wr)
}, 3*time.Second, 300*time.Millisecond).Should(BeNil())
By("Check workload should only have 1 generation")
Expect(wr.GetGeneration()).Should(BeEquivalentTo(1))
By("Check reconcile again and no error will happen")
reconcileRetry(reconciler, req)
Expect(k8sClient.Get(ctx, appConfigKey, &appConfig)).Should(BeNil())
By("===================================== Start to Upgrade revision of component =========================================")
cmpV2 := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV2)).Should(Succeed())
cmpV2.Spec.Workload = runtime.RawExtension{
Object: getDeploy("wordpress:v2", specifiedNameV1),
}
By("Update Component")
Expect(k8sClient.Update(ctx, cmpV2)).Should(Succeed())
By("component handler will automatically create a ne controller revision")
Expect(func() bool { _, ok := componentHandler.createControllerRevision(cmpV2, cmpV2); return ok }()).Should(BeTrue())
By("Check controller revision created successfully")
Eventually(func() error {
labels := &metav1.LabelSelector{
MatchLabels: map[string]string{
ControllerRevisionComponentLabel: compName,
},
}
selector, err := metav1.LabelSelectorAsSelector(labels)
if err != nil {
return err
}
err = k8sClient.List(ctx, &crList, &client.ListOptions{
LabelSelector: selector,
})
if err != nil {
return err
}
if len(crList.Items) != 2 {
return fmt.Errorf("there should be exactly 2 revision created but got %d", len(crList.Items))
}
return nil
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Update appConfig & check successfully")
appConfig.Spec.Components[0].RevisionName = compName + "-v2"
Expect(k8sClient.Update(ctx, &appConfig)).Should(Succeed())
Eventually(func() error {
return k8sClient.Get(ctx, appConfigKey, &appConfig)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Reconcile for new revision")
reconcileRetry(reconciler, req)
By("Check new revision workload created successfully")
Eventually(func() error {
reconcileRetry(reconciler, req)
var workloadKey = client.ObjectKey{Namespace: namespace, Name: specifiedNameV1}
return k8sClient.Get(ctx, workloadKey, &wr)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Check the new workload should only have 1 generation")
Expect(wr.GetGeneration()).Should(BeEquivalentTo(1))
Expect(wr.Spec.Template.Spec.Containers[0].Image).Should(BeEquivalentTo("wordpress:v2"))
By("Check the new workload should only have 1 generation")
Expect(wr.GetGeneration()).Should(BeEquivalentTo(1))
By("Check reconcile again")
reconcileRetry(reconciler, req)
By("Check appconfig condition should have error")
Eventually(func() string {
reconcileRetry(reconciler, req)
err := k8sClient.Get(ctx, appConfigKey, &appConfig)
if err != nil {
return err.Error()
}
if len(appConfig.Status.Conditions) != 1 {
return "condition len should be 1 but now is " + strconv.Itoa(len(appConfig.Status.Conditions))
}
By(fmt.Sprintf("Reconcile with condition %v", appConfig.Status.Conditions[0]))
return string(appConfig.Status.Conditions[0].Reason)
}, 3*time.Second, 300*time.Millisecond).Should(BeEquivalentTo("ReconcileError"))
By("Check the old workload still there")
Eventually(func() error {
reconcileRetry(reconciler, req)
var workloadKey = client.ObjectKey{Namespace: namespace, Name: specifiedNameBase}
return k8sClient.Get(ctx, workloadKey, &wr)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Check the old workload should only have 1 generation")
Expect(wr.GetGeneration()).Should(BeEquivalentTo(1))
Expect(wr.Spec.Template.Spec.Containers[0].Image).Should(BeEquivalentTo("wordpress:4.6.1-apache"))
wr.Spec.Replicas = pointer.Int32Ptr(0)
Expect(k8sClient.Update(ctx, &wr)).Should(Succeed())
By("Reconcile Again and appconfig condition should not have error")
Eventually(func() string {
By("Once more Reconcile and should not have error")
reconcileRetry(reconciler, req)
err := k8sClient.Get(ctx, appConfigKey, &appConfig)
if err != nil {
return err.Error()
}
if len(appConfig.Status.Conditions) != 1 {
return "condition len should be 1 but now is " + strconv.Itoa(len(appConfig.Status.Conditions))
}
return string(appConfig.Status.Conditions[0].Reason)
}, 3*time.Second, 300*time.Millisecond).Should(BeEquivalentTo("ReconcileSuccess"))
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: specifiedNameBase}, &wr)).Should(SatisfyAny(util.NotFoundMatcher{}))
By("===================================== Start to Upgrade revision of component again =========================================")
cmpV3 := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV3)).Should(Succeed())
cmpV3.Spec.Workload = runtime.RawExtension{
Object: getDeploy("wordpress:v3", specifiedNameV2),
}
By("Update Component")
Expect(k8sClient.Update(ctx, cmpV3)).Should(Succeed())
By("component handler will automatically create a ne controller revision")
Expect(func() bool { _, ok := componentHandler.createControllerRevision(cmpV3, cmpV3); return ok }()).Should(BeTrue())
By("Update the AC and add the revisionEnabled Trait")
appConfig.Spec.Components[0].RevisionName = compName + "-v3"
appConfig.Spec.Components[0].Traits = []v1alpha2.ComponentTrait{
{Trait: runtime.RawExtension{Object: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "example.com/v1",
"kind": "Foo",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"trait.oam.dev/type": "rollout-revision",
},
},
"spec": map[string]interface{}{
"key": "test3",
},
}}}},
}
Expect(k8sClient.Update(ctx, &appConfig)).Should(Succeed())
Eventually(func() error {
return k8sClient.Get(ctx, appConfigKey, &appConfig)
}, time.Second, 300*time.Millisecond).Should(BeNil())
Expect(len(appConfig.Spec.Components[0].Traits)).Should(BeEquivalentTo(1))
By("Check reconcile again and no error will happen, revisionEnabled will skip delete")
reconcileRetry(reconciler, req)
By("Check appconfig condition should not have error")
Eventually(func() string {
By("Once more Reconcile and should not have error")
reconcileRetry(reconciler, req)
err := k8sClient.Get(ctx, appConfigKey, &appConfig)
if err != nil {
return err.Error()
}
if len(appConfig.Status.Conditions) != 1 {
return "condition len should be 1 but now is " + strconv.Itoa(len(appConfig.Status.Conditions))
}
return string(appConfig.Status.Conditions[0].Reason)
}, 3*time.Second, 300*time.Millisecond).Should(BeEquivalentTo("ReconcileSuccess"))
By("Check new revision workload created successfully")
Eventually(func() error {
reconcileRetry(reconciler, req)
var workloadKey = client.ObjectKey{Namespace: namespace, Name: specifiedNameV2}
return k8sClient.Get(ctx, workloadKey, &wr)
}, time.Second, 300*time.Millisecond).Should(BeNil())
By("Check the new workload should only have 1 generation")
Expect(wr.GetGeneration()).Should(BeEquivalentTo(1))
Expect(wr.Spec.Template.Spec.Containers[0].Image).Should(BeEquivalentTo("wordpress:v3"))
By("Check the new workload should only have 1 generation")
Expect(wr.GetGeneration()).Should(BeEquivalentTo(1))
By("Check the old workload still there")
Eventually(func() error {
reconcileRetry(reconciler, req)
var workloadKey = client.ObjectKey{Namespace: namespace, Name: specifiedNameV1}
return k8sClient.Get(ctx, workloadKey, &wr)
}, time.Second, 300*time.Millisecond).Should(BeNil())
reconciler.applyOnceOnlyMode = "off"
})
})

View File

@@ -2,6 +2,7 @@ package applicationconfiguration
import (
"context"
"os"
"path/filepath"
"testing"
"time"
@@ -10,6 +11,7 @@ import (
. "github.com/onsi/gomega"
"github.com/crossplane/crossplane-runtime/pkg/logging"
corev1 "k8s.io/api/core/v1"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -44,7 +46,7 @@ var k8sClient client.Client
var scheme = runtime.NewScheme()
var crd crdv1.CustomResourceDefinition
func TestReconcilder(t *testing.T) {
func TestReconcilerSuit(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
@@ -55,9 +57,16 @@ func TestReconcilder(t *testing.T) {
var _ = BeforeSuite(func(done Done) {
ctx := context.Background()
By("Bootstrapping test environment")
var yamlPath string
if _, set := os.LookupEnv("COMPATIBILITY_TEST"); set {
yamlPath = "../../../../../test/compatibility-test/testdata"
} else {
yamlPath = filepath.Join("../../../../..", "charts", "vela-core", "crds")
}
logf.Log.Info("start applicationconfiguration suit test", "yaml_path", yamlPath)
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("../../../../..", "charts/vela-core/crds"), // this has all the required CRDs,
yamlPath, // this has all the required CRDs,
},
}
var err error
@@ -150,13 +159,14 @@ var _ = BeforeSuite(func(done Done) {
}, time.Second*30, time.Millisecond*500).Should(BeNil())
Expect(mapping.Resource.Resource).Should(Equal("foo"))
reconciler = NewReconciler(mgr, dm, WithLogger(logging.NewLogrLogger(ctrl.Log.WithName("suit-test-appconfig"))))
reconciler = NewReconciler(mgr, dm, logging.NewLogrLogger(ctrl.Log.WithName("suit-test-appconfig")))
componentHandler = &ComponentHandler{Client: k8sClient, RevisionLimit: 100, Logger: logging.NewLogrLogger(ctrl.Log.WithName("component-handler"))}
By("Creating workload definition and trait definition")
wd := v1alpha2.WorkloadDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "foo.example.com",
Name: "foo.example.com",
Namespace: "vela-system",
},
Spec: v1alpha2.WorkloadDefinitionSpec{
Reference: v1alpha2.DefinitionReference{
@@ -166,7 +176,8 @@ var _ = BeforeSuite(func(done Done) {
}
td := v1alpha2.TraitDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "foo.example.com",
Name: "foo.example.com",
Namespace: "vela-system",
},
Spec: v1alpha2.TraitDefinitionSpec{
Reference: v1alpha2.DefinitionReference{
@@ -177,7 +188,8 @@ var _ = BeforeSuite(func(done Done) {
rollout := v1alpha2.TraitDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "rollout-revision",
Name: "rollout-revision",
Namespace: "vela-system",
},
Spec: v1alpha2.TraitDefinitionSpec{
Reference: v1alpha2.DefinitionReference{
@@ -186,6 +198,8 @@ var _ = BeforeSuite(func(done Done) {
RevisionEnabled: true,
},
}
definitonNs := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}}
Expect(k8sClient.Create(context.Background(), definitonNs.DeepCopy())).Should(BeNil())
// For some reason, WorkloadDefinition is created as a Cluster scope object
Expect(k8sClient.Create(ctx, &wd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))

View File

@@ -31,7 +31,8 @@ func (r *Reconciler) extractWorkloadTypeAndGVK(ctx context.Context, componentLis
}
// get the workload definition
// the validator webhook has checked that source and the target are the same type
wd, err := oamutil.GetWorkloadDefinition(ctx, r, componentType)
wd := new(corev1alpha2.WorkloadDefinition)
err := oamutil.GetDefinition(ctx, r, wd, componentType)
if err != nil {
return "", nil, errors.Wrap(err, fmt.Sprintf("failed to get workload definition %s", componentType))
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
const appDeployFinalizer = "finalizers.applicationdeployment.oam.dev"
@@ -71,6 +72,8 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr e
// TODO: check if the target/source has changed
r.handleFinalizer(&appDeploy)
ctx = oamutil.SetNnamespaceInCtx(ctx, appDeploy.Namespace)
// Get the target application
var targetApp corev1alpha2.Application
var sourceApp *corev1alpha2.Application

View File

@@ -1,6 +1,7 @@
package applicationdeployment
import (
"os"
"path/filepath"
"testing"
@@ -37,9 +38,16 @@ var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
By("bootstrapping test environment")
var yamlPath string
if _, set := os.LookupEnv("COMPATIBILITY_TEST"); set {
yamlPath = "../../../../../test/compatibility-test/testdata"
} else {
yamlPath = filepath.Join("../../../../..", "charts", "vela-core", "crds")
}
logf.Log.Info("start application deployment suit test", "yaml_path", yamlPath)
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("../../../..", "charts/vela-core/crds"), // this has all the required CRDs,
yamlPath, // this has all the required CRDs,
filepath.Join("..", "config", "crd", "bases")},
}

View File

@@ -94,6 +94,9 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
if err := r.Get(ctx, req.NamespacedName, &manualScalar); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
ctx = util.SetNnamespaceInCtx(ctx, manualScalar.Namespace)
r.log.Info("Get the manualscalar trait", "ReplicaCount", manualScalar.Spec.ReplicaCount,
"Annotations", manualScalar.GetAnnotations())
// find the resource object to record the event to, default is the parent appConfig.

View File

@@ -20,10 +20,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"github.com/oam-dev/kubevela/pkg/controller/common"
"github.com/oam-dev/kubevela/pkg/controller/standard.oam.dev/v1alpha1/autoscaler"
"github.com/oam-dev/kubevela/pkg/controller/standard.oam.dev/v1alpha1/metrics"
"github.com/oam-dev/kubevela/pkg/controller/standard.oam.dev/v1alpha1/podspecworkload"
"github.com/oam-dev/kubevela/pkg/controller/standard.oam.dev/v1alpha1/routes"
"github.com/oam-dev/kubevela/pkg/controller/utils"
)
@@ -33,23 +30,14 @@ func Setup(mgr ctrl.Manager, disableCaps string) error {
switch disableCaps {
case common.DisableNoneCaps:
functions = []func(ctrl.Manager) error{
metrics.Setup, podspecworkload.Setup, routes.Setup, autoscaler.Setup,
podspecworkload.Setup,
}
case common.DisableAllCaps:
default:
disableCapsSet := utils.StoreInSet(disableCaps)
if !disableCapsSet.Contains(common.MetricsControllerName) {
functions = append(functions, metrics.Setup)
}
if !disableCapsSet.Contains(common.PodspecWorkloadControllerName) {
functions = append(functions, podspecworkload.Setup)
}
if !disableCapsSet.Contains(common.RouteControllerName) {
functions = append(functions, routes.Setup)
}
if !disableCapsSet.Contains(common.AutoscaleControllerName) {
functions = append(functions, autoscaler.Setup)
}
}
for _, setup := range functions {

View File

@@ -1,163 +0,0 @@
/*
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 autoscaler
import (
"context"
"fmt"
"time"
cpv1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/go-logr/logr"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/pkg/controller/common"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
// nolint:golint
const (
SpecWarningTargetWorkloadNotSet = "Spec.targetWorkload is not set"
SpecWarningStartAtTimeFormat = "startAt is not in the right format, which should be like `12:01`"
SpecWarningStartAtTimeRequired = "spec.triggers.condition.startAt: Required value"
SpecWarningDurationTimeRequired = "spec.triggers.condition.duration: Required value"
SpecWarningReplicasRequired = "spec.triggers.condition.replicas: Required value"
SpecWarningDurationTimeNotInRightFormat = "spec.triggers.condition.duration: not in the right format"
)
// ReconcileWaitResult is the time to wait between reconciliation.
var ReconcileWaitResult = reconcile.Result{RequeueAfter: 30 * time.Second}
// Reconciler reconciles a Autoscaler object
type Reconciler struct {
client.Client
dm discoverymapper.DiscoveryMapper
Log logr.Logger
Scheme *runtime.Scheme
record event.Recorder
}
// Reconcile is the main logic for autoscaler controller
// +kubebuilder:rbac:groups=standard.oam.dev,resources=autoscalers,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=standard.oam.dev,resources=autoscalers/status,verbs=get;update;patch
func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("autoscaler", req.NamespacedName)
log.Info("Reconciling Autoscaler...")
ctx := context.Background()
var scaler v1alpha1.Autoscaler
if err := r.Get(ctx, req.NamespacedName, &scaler); err != nil {
log.Error(err, "Failed to get trait", "traitName", scaler.Name)
return ReconcileWaitResult, client.IgnoreNotFound(err)
}
log.Info("Retrieved trait Autoscaler", "APIVersion", scaler.APIVersion, "Kind", scaler.Kind)
// find the resource object to record the event to, default is the parent appConfig.
eventObj, err := util.LocateParentAppConfig(ctx, r.Client, &scaler)
if err != nil {
log.Error(err, "Failed to find the parent resource", "Autoscaler", scaler.Name)
return util.ReconcileWaitResult, util.PatchCondition(ctx, r, &scaler,
cpv1alpha1.ReconcileError(fmt.Errorf(util.ErrLocateAppConfig)))
}
if eventObj == nil {
// fallback to workload itself
log.Info("There is no parent resource", "Autoscaler", scaler.Name)
eventObj = &scaler
}
// Fetch the instance to which the trait refers to
workload, err := util.FetchWorkload(ctx, r, log, &scaler)
if err != nil {
log.Error(err, "Error while fetching the workload", "workload reference",
scaler.GetWorkloadReference())
r.record.Event(&scaler, event.Warning(common.ErrLocatingWorkload, err))
return util.ReconcileWaitResult,
util.PatchCondition(ctx, r, &scaler,
cpv1alpha1.ReconcileError(errors.Wrap(err, common.ErrLocatingWorkload)))
}
// Fetch the child resources list from the corresponding workload
resources, err := util.FetchWorkloadChildResources(ctx, log, r, r.dm, workload)
if err != nil {
log.Error(err, "Error while fetching the workload child resources", "workload", workload.UnstructuredContent())
r.record.Event(eventObj, event.Warning(util.ErrFetchChildResources, err))
return util.ReconcileWaitResult, util.PatchCondition(ctx, r, &scaler,
cpv1alpha1.ReconcileError(fmt.Errorf(util.ErrFetchChildResources)))
}
resources = append(resources, workload)
targetWorkloadSetFlag := false
for _, res := range resources {
// Keda only support these four built-in workload now.
if res.GetKind() == "Deployment" || res.GetKind() == "StatefulSet" || res.GetKind() == "DaemonSet" || res.GetKind() == "ReplicaSet" {
scaler.Spec.TargetWorkload = v1alpha1.TargetWorkload{
APIVersion: res.GetAPIVersion(),
Kind: res.GetKind(),
Name: res.GetName(),
}
targetWorkloadSetFlag = true
break
}
}
// if no child resource found, set the workload as target workload
if !targetWorkloadSetFlag {
scaler.Spec.TargetWorkload = v1alpha1.TargetWorkload{
APIVersion: workload.GetAPIVersion(),
Kind: workload.GetKind(),
Name: workload.GetName(),
}
}
namespace := req.NamespacedName.Namespace
if err := r.scaleByKEDA(scaler, namespace, log); err != nil {
return ReconcileWaitResult, err
}
return ctrl.Result{}, nil
}
// SetupWithManager will setup with event recorder
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor("Autoscaler")).
WithAnnotations("controller", "Autoscaler")
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.Autoscaler{}).
Complete(r)
}
// Setup adds a controller that reconciles MetricsTrait.
func Setup(mgr ctrl.Manager) error {
dm, err := discoverymapper.New(mgr.GetConfig())
if err != nil {
return err
}
r := Reconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("Autoscaler"),
Scheme: mgr.GetScheme(),
dm: dm,
}
return r.SetupWithManager(mgr)
}

View File

@@ -1,223 +0,0 @@
package autoscaler
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/go-logr/logr"
"github.com/pkg/errors"
kedav1alpha1 "github.com/wonderflow/keda-api/api/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
)
func (r *Reconciler) scaleByKEDA(scaler v1alpha1.Autoscaler, namespace string, log logr.Logger) error {
ctx := context.Background()
minReplicas := scaler.Spec.MinReplicas
maxReplicas := scaler.Spec.MaxReplicas
triggers := scaler.Spec.Triggers
scalerName := scaler.Name
targetWorkload := scaler.Spec.TargetWorkload
var kedaTriggers []kedav1alpha1.ScaleTriggers
var err error
for _, t := range triggers {
if t.Type == CronType {
cronKedaTriggers, reason, err := r.prepareKEDACronScalerTriggerSpec(scaler, t)
if err != nil {
log.Error(err, reason)
r.record.Event(&scaler, event.Warning(event.Reason(reason), err))
return err
}
kedaTriggers = append(kedaTriggers, cronKedaTriggers...)
} else {
kedaTriggers = append(kedaTriggers, kedav1alpha1.ScaleTriggers{
Type: string(t.Type),
Name: t.Name,
Metadata: t.Condition,
// TODO(wonderflow): add auth in the future
AuthenticationRef: nil,
})
}
}
spec := kedav1alpha1.ScaledObjectSpec{
ScaleTargetRef: &kedav1alpha1.ScaleTarget{
APIVersion: targetWorkload.APIVersion,
Kind: targetWorkload.Kind,
Name: targetWorkload.Name,
},
MinReplicaCount: minReplicas,
MaxReplicaCount: maxReplicas,
Triggers: kedaTriggers,
}
var scaleObj kedav1alpha1.ScaledObject
err = r.Client.Get(ctx, types.NamespacedName{Name: scalerName, Namespace: namespace}, &scaleObj)
if err != nil {
if apierrors.IsNotFound(err) {
scaleObj := kedav1alpha1.ScaledObject{
ObjectMeta: metav1.ObjectMeta{
Name: scalerName,
Namespace: namespace,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: scaler.APIVersion,
Kind: scaler.Kind,
UID: scaler.GetUID(),
Name: scalerName,
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
Spec: spec,
}
if err := r.Client.Create(ctx, &scaleObj); err != nil {
log.Error(err, "failed to create KEDA ScaledObj", "ScaledObject", scaleObj)
return err
}
log.Info("KEDA ScaledObj created", "ScaledObjectName", scalerName)
}
} else {
scaleObj.Spec = spec
if err := r.Client.Update(ctx, &scaleObj); err != nil {
log.Error(err, "failed to update KEDA ScaledObj", "ScaledObject", scaleObj)
return err
}
log.Info("KEDA ScaledObj updated", "ScaledObjectName", scalerName)
}
return nil
}
// CronTypeCondition defines the cron type for autoscaler
type CronTypeCondition struct {
// StartAt is the time when the scaler starts, in format `"HHMM"` for example, "08:00"
StartAt string `json:"startAt,omitempty"`
// Duration means how long the target scaling will keep, after the time of duration, the scaling will stop
Duration string `json:"duration,omitempty"`
// Days means in which days the condition will take effect
Days string `json:"days,omitempty"`
// Replicas is the expected replicas
Replicas string `json:"replicas,omitempty"`
// Timezone defines the time zone, default to the timezone of the Kubernetes cluster
Timezone string `json:"timezone,omitempty"`
}
// GetCronTypeCondition will get condition from map
func GetCronTypeCondition(condition map[string]string) (*CronTypeCondition, error) {
data, err := json.Marshal(condition)
if err != nil {
return nil, err
}
var cronCon CronTypeCondition
if err = json.Unmarshal(data, &cronCon); err != nil {
return nil, err
}
return &cronCon, nil
}
// prepareKEDACronScalerTriggerSpec converts Autoscaler spec into KEDA Cron scaler spec
func (r *Reconciler) prepareKEDACronScalerTriggerSpec(scaler v1alpha1.Autoscaler, t v1alpha1.Trigger) ([]kedav1alpha1.ScaleTriggers, string, error) {
var kedaTriggers []kedav1alpha1.ScaleTriggers
targetWorkload := scaler.Spec.TargetWorkload
if targetWorkload.Name == "" {
err := errors.New(SpecWarningTargetWorkloadNotSet)
return kedaTriggers, SpecWarningTargetWorkloadNotSet, err
}
triggerCondition, err := GetCronTypeCondition(t.Condition)
if err != nil {
return nil, "convert cron condition failed", err
}
startAt := triggerCondition.StartAt
if startAt == "" {
return kedaTriggers, SpecWarningStartAtTimeRequired, errors.New(SpecWarningStartAtTimeRequired)
}
duration := triggerCondition.Duration
if duration == "" {
return kedaTriggers, SpecWarningDurationTimeRequired, errors.New(SpecWarningDurationTimeRequired)
}
startTime, err := time.Parse("15:04", startAt)
if err != nil {
return kedaTriggers, SpecWarningStartAtTimeFormat, err
}
var startHour, startMinute int
startHour = startTime.Hour()
startMinute = startTime.Minute()
durationTime, err := time.ParseDuration(duration)
if err != nil {
return kedaTriggers, SpecWarningDurationTimeNotInRightFormat, err
}
durationHour := durationTime.Hours()
durationMin := int(durationTime.Minutes()) % 60
endMinite := startMinute + durationMin
endHour := int(durationHour) + startHour
if endMinite >= 60 {
endMinite %= 60
endHour++
}
var durationOneMoreDay int
if endHour >= 24 {
endHour %= 24
durationOneMoreDay = 1
}
replicas, err := strconv.Atoi(triggerCondition.Replicas)
if err != nil {
return nil, "parse replica failed", err
}
if replicas == 0 {
return kedaTriggers, SpecWarningReplicasRequired, errors.New(SpecWarningReplicasRequired)
}
timezone := triggerCondition.Timezone
days := strings.Split(triggerCondition.Days, ",")
var dayNo []int
for i, d := range days {
d = strings.TrimSpace(d)
days[i] = d
var found = false
for i := 0; i < 7; i++ {
if strings.EqualFold(time.Weekday(i).String(), d) {
dayNo = append(dayNo, i)
found = true
break
}
}
if !found {
return nil, "", fmt.Errorf("wrong format %s, should be one of %v", d,
[]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"})
}
}
for idx, n := range dayNo {
kedaTrigger := kedav1alpha1.ScaleTriggers{
Type: string(t.Type),
Name: t.Name + "-" + days[idx],
Metadata: map[string]string{
"timezone": timezone,
"start": fmt.Sprintf("%d %d * * %d", startMinute, startHour, n),
"end": fmt.Sprintf("%d %d * * %d", endMinite, endHour, (n+durationOneMoreDay)%7),
"desiredReplicas": strconv.Itoa(replicas),
},
}
kedaTriggers = append(kedaTriggers, kedaTrigger)
}
return kedaTriggers, "", nil
}

View File

@@ -1,81 +0,0 @@
/*
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 autoscaler
import (
"path/filepath"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
standardv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
// +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
err = standardv1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
close(done)
}, 60)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})

View File

@@ -1,11 +0,0 @@
package autoscaler
import (
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
)
// constants used in autoscaler controller
const (
CronType v1alpha1.TriggerType = "cron"
CPUType v1alpha1.TriggerType = "cpu"
)

View File

@@ -1,366 +0,0 @@
/*
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 metrics
import (
"context"
"fmt"
"reflect"
monitoring "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1"
cpv1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/go-logr/logr"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/util/retry"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/pkg/controller/common"
"github.com/oam-dev/kubevela/pkg/controller/utils"
)
const (
errApplyServiceMonitor = "failed to apply the service monitor"
errFailDiscoveryLabels = "failed to discover labels from pod template, use workload labels directly"
servicePort = 4848
)
var (
serviceMonitorKind = reflect.TypeOf(monitoring.ServiceMonitor{}).Name()
serviceMonitorAPIVersion = monitoring.SchemeGroupVersion.String()
)
var (
// ServiceMonitorNSName is the name of the namespace in which the serviceMonitor resides
// it must be the same that the prometheus operator is listening to
ServiceMonitorNSName = "monitoring"
)
// GetOAMServiceLabel will return oamServiceLabel as the pre-defined labels for any serviceMonitor
// created by the MetricsTrait, prometheus operator listens on this
func GetOAMServiceLabel() map[string]string {
return map[string]string{
"k8s-app": "oam",
"controller": "metricsTrait",
}
}
// Reconciler reconciles a MetricsTrait object
type Reconciler struct {
client.Client
dm discoverymapper.DiscoveryMapper
Log logr.Logger
Scheme *runtime.Scheme
record event.Recorder
}
// Reconcile is the main logic for metric trait controller
// +kubebuilder:rbac:groups=standard.oam.dev,resources=metricstraits,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=standard.oam.dev,resources=metricstraits/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=*,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=*/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core.oam.dev,resources=*,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core.oam.dev,resources=*/status,verbs=get;
// +kubebuilder:rbac:groups="",resources=events,verbs=get;list;create;update;patch
func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
mLog := r.Log.WithValues("metricstrait", req.NamespacedName)
mLog.Info("Reconcile metricstrait trait")
// fetch the trait
var metricsTrait v1alpha1.MetricsTrait
if err := r.Get(ctx, req.NamespacedName, &metricsTrait); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
mLog.Info("Get the metricsTrait trait",
"metrics end point", metricsTrait.Spec.ScrapeService,
"workload reference", metricsTrait.Spec.WorkloadReference,
"labels", metricsTrait.GetLabels())
// find the resource object to record the event to, default is the parent appConfig.
eventObj, err := oamutil.LocateParentAppConfig(ctx, r.Client, &metricsTrait)
if eventObj == nil {
// fallback to workload itself
mLog.Error(err, "add events to metricsTrait itself", "name", metricsTrait.Name)
eventObj = &metricsTrait
}
if metricsTrait.Spec.ScrapeService.Enabled != nil && !*metricsTrait.Spec.ScrapeService.Enabled {
r.record.Event(eventObj, event.Normal("Metrics Trait disabled", "no op"))
r.gcOrphanServiceMonitor(ctx, mLog, &metricsTrait)
return ctrl.Result{}, oamutil.PatchCondition(ctx, r, &metricsTrait, cpv1alpha1.ReconcileSuccess())
}
// Fetch the workload instance to which we want to expose metrics
workload, err := oamutil.FetchWorkload(ctx, r, mLog, &metricsTrait)
if err != nil {
mLog.Error(err, "Error while fetching the workload", "workload reference",
metricsTrait.GetWorkloadReference())
r.record.Event(eventObj, event.Warning(common.ErrLocatingWorkload, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &metricsTrait,
cpv1alpha1.ReconcileError(errors.Wrap(err, common.ErrLocatingWorkload)))
}
var targetPort = metricsTrait.Spec.ScrapeService.TargetPort
// try to see if the workload already has services as child resources
serviceLabel, err := r.fetchServicesLabel(ctx, mLog, workload, targetPort)
if err != nil && !apierrors.IsNotFound(err) {
r.record.Event(eventObj, event.Warning(common.ErrLocatingService, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &metricsTrait,
cpv1alpha1.ReconcileError(errors.Wrap(err, common.ErrLocatingService)))
} else if serviceLabel == nil {
// TODO: use podMonitor instead?
// no service with the targetPort found, we will create a service that talks to the targetPort
serviceLabel, targetPort, err = r.createService(ctx, mLog, workload, &metricsTrait)
if err != nil {
r.record.Event(eventObj, event.Warning(common.ErrCreatingService, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &metricsTrait,
cpv1alpha1.ReconcileError(errors.Wrap(err, common.ErrCreatingService)))
}
}
metricsTrait.Status.Port = targetPort
metricsTrait.Status.SelectorLabels = serviceLabel
// construct the serviceMonitor that hooks the service to the prometheus server
serviceMonitor := constructServiceMonitor(&metricsTrait, targetPort)
// server side apply the serviceMonitor, only the fields we set are touched
applyOpts := []client.PatchOption{client.ForceOwnership, client.FieldOwner(metricsTrait.GetUID())}
if err := r.Patch(ctx, serviceMonitor, client.Apply, applyOpts...); err != nil {
mLog.Error(err, "Failed to apply to serviceMonitor")
r.record.Event(eventObj, event.Warning(errApplyServiceMonitor, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &metricsTrait,
cpv1alpha1.ReconcileError(errors.Wrap(err, errApplyServiceMonitor)))
}
r.record.Event(eventObj, event.Normal("ServiceMonitor created",
fmt.Sprintf("successfully server side patched a serviceMonitor `%s`", serviceMonitor.Name)))
r.gcOrphanServiceMonitor(ctx, mLog, &metricsTrait)
(&metricsTrait).SetConditions(cpv1alpha1.ReconcileSuccess())
return ctrl.Result{}, errors.Wrap(r.UpdateStatus(ctx, &metricsTrait), common.ErrUpdateStatus)
}
// fetch the label of the service that is associated with the workload
func (r *Reconciler) fetchServicesLabel(ctx context.Context, mLog logr.Logger,
workload *unstructured.Unstructured, targetPort intstr.IntOrString) (map[string]string, error) {
// Fetch the child resources list from the corresponding workload
resources, err := oamutil.FetchWorkloadChildResources(ctx, mLog, r, r.dm, workload)
if err != nil {
if !apierrors.IsNotFound(err) {
mLog.Error(err, "Error while fetching the workload child resources", "workload kind", workload.GetKind(),
"workload name", workload.GetName())
}
return nil, err
}
// find the service that has the port
for _, childRes := range resources {
if childRes.GetAPIVersion() == common.ServiceAPIVersion && childRes.GetKind() == common.ServiceKind {
ports, _, _ := unstructured.NestedSlice(childRes.Object, "spec", "ports")
for _, port := range ports {
servicePort, _ := port.(corev1.ServicePort)
if servicePort.TargetPort == targetPort {
return childRes.GetLabels(), nil
}
}
}
}
return nil, nil
}
// create a service that targets the exposed workload pod
func (r *Reconciler) createService(ctx context.Context, mLog logr.Logger, workload *unstructured.Unstructured,
metricsTrait *v1alpha1.MetricsTrait) (map[string]string, intstr.IntOrString, error) {
oamService := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: common.ServiceKind,
APIVersion: common.ServiceAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "oam-" + workload.GetName(),
Namespace: workload.GetNamespace(),
Labels: GetOAMServiceLabel(),
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: metricsTrait.GetObjectKind().GroupVersionKind().GroupVersion().String(),
Kind: metricsTrait.GetObjectKind().GroupVersionKind().Kind,
UID: metricsTrait.GetUID(),
Name: metricsTrait.GetName(),
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
},
}
var targetPort = metricsTrait.Spec.ScrapeService.TargetPort
// assign selector
ports, labels, err := utils.DiscoveryFromPodTemplate(workload, "spec", "template")
if err != nil {
mLog.Info(errFailDiscoveryLabels, "err", err)
if len(metricsTrait.Spec.ScrapeService.TargetSelector) == 0 {
// we assumed that the pods have the same label as the workload if no discoverable
oamService.Spec.Selector = workload.GetLabels()
} else {
oamService.Spec.Selector = metricsTrait.Spec.ScrapeService.TargetSelector
}
} else {
oamService.Spec.Selector = labels
}
if targetPort.String() == "0" {
if len(ports) == 0 {
return nil, intstr.IntOrString{}, fmt.Errorf("no ports discovered or specified")
}
// choose the first one if no port specified
targetPort = ports[0]
}
oamService.Spec.Ports = []corev1.ServicePort{
{
Port: servicePort,
TargetPort: targetPort,
Protocol: corev1.ProtocolTCP,
},
}
// server side apply the service, only the fields we set are touched
applyOpts := []client.PatchOption{client.ForceOwnership, client.FieldOwner(metricsTrait.GetUID())}
if err := r.Patch(ctx, oamService, client.Apply, applyOpts...); err != nil {
mLog.Error(err, "Failed to apply to service")
return nil, intstr.IntOrString{}, err
}
return oamService.Spec.Selector, targetPort, nil
}
// remove all service monitors that are no longer used
func (r *Reconciler) gcOrphanServiceMonitor(ctx context.Context, mLog logr.Logger,
metricsTrait *v1alpha1.MetricsTrait) {
var gcCandidate = metricsTrait.Status.ServiceMonitorName
if metricsTrait.Spec.ScrapeService.Enabled != nil && !*metricsTrait.Spec.ScrapeService.Enabled {
// initialize it to be an empty list, gc everything
metricsTrait.Status.ServiceMonitorName = ""
} else {
// re-initialize to the current service monitor
metricsTrait.Status.ServiceMonitorName = metricsTrait.Name
}
if gcCandidate == metricsTrait.Name {
return
}
if err := r.Delete(ctx, &monitoring.ServiceMonitor{
TypeMeta: metav1.TypeMeta{
Kind: serviceMonitorKind,
APIVersion: serviceMonitorAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: gcCandidate,
Namespace: metricsTrait.GetNamespace(),
},
}, client.GracePeriodSeconds(10)); err != nil {
mLog.Error(err, "Failed to delete serviceMonitor", "name", gcCandidate, "error", err)
}
}
// construct a serviceMonitor given a metrics trait along with a label selector pointing to the underlying service
func constructServiceMonitor(metricsTrait *v1alpha1.MetricsTrait, targetPort intstr.IntOrString) *monitoring.ServiceMonitor {
return &monitoring.ServiceMonitor{
TypeMeta: metav1.TypeMeta{
Kind: serviceMonitorKind,
APIVersion: serviceMonitorAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: metricsTrait.Name,
Namespace: ServiceMonitorNSName,
Labels: GetOAMServiceLabel(),
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: metricsTrait.GetObjectKind().GroupVersionKind().GroupVersion().String(),
Kind: metricsTrait.GetObjectKind().GroupVersionKind().Kind,
UID: metricsTrait.GetUID(),
Name: metricsTrait.GetName(),
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
Spec: monitoring.ServiceMonitorSpec{
Selector: metav1.LabelSelector{
MatchLabels: GetOAMServiceLabel(),
},
// we assumed that the service is in the same namespace as the trait
NamespaceSelector: monitoring.NamespaceSelector{
MatchNames: []string{metricsTrait.Namespace},
},
Endpoints: []monitoring.Endpoint{
{
TargetPort: &targetPort,
Path: metricsTrait.Spec.ScrapeService.Path,
Scheme: metricsTrait.Spec.ScrapeService.Scheme,
},
},
},
}
}
// SetupWithManager setup Reconciler with ctrl.Manager
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor("MetricsTrait")).
WithAnnotations("controller", "metricsTrait")
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.MetricsTrait{}).
Owns(&monitoring.ServiceMonitor{}).
Complete(r)
}
// UpdateStatus updates v1alpha1.MetricsTrait's Status with retry.RetryOnConflict
func (r *Reconciler) UpdateStatus(ctx context.Context, mt *v1alpha1.MetricsTrait, opts ...client.UpdateOption) error {
status := mt.DeepCopy().Status
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
if err = r.Get(ctx, types.NamespacedName{Namespace: mt.Namespace, Name: mt.Name}, mt); err != nil {
return
}
mt.Status = status
return r.Status().Update(ctx, mt, opts...)
})
}
// Setup adds a controller that reconciles MetricsTrait.
func Setup(mgr ctrl.Manager) error {
dm, err := discoverymapper.New(mgr.GetConfig())
if err != nil {
return err
}
reconciler := Reconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("MetricsTrait"),
Scheme: mgr.GetScheme(),
dm: dm,
}
return reconciler.SetupWithManager(mgr)
}

View File

@@ -1,218 +0,0 @@
package metrics
import (
"context"
"reflect"
"time"
monitoringv1 "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1"
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
)
var (
metricsTraitKind = reflect.TypeOf(v1alpha1.MetricsTrait{}).Name()
metricsTraitAPIVersion = v1alpha1.SchemeGroupVersion.String()
deploymentKind = reflect.TypeOf(appsv1.Deployment{}).Name()
deploymentAPIVersion = appsv1.SchemeGroupVersion.String()
)
var _ = Describe("Metrics Trait Integration Test", func() {
// common var init
ctx := context.Background()
namespaceName := "metricstrait-integration-test"
traitLabel := map[string]string{"trait": "metricsTraitBase"}
deployLabel := map[string]string{"standard.oam.dev": "oam-test-deployment"}
podPort := 8080
targetPort := intstr.FromInt(podPort)
metricsPath := "/notMetrics"
scheme := "http"
var ns corev1.Namespace
var metricsTraitBase v1alpha1.MetricsTrait
var workloadBase appsv1.Deployment
BeforeEach(func() {
logf.Log.Info("[TEST] Set up resources before an integration test")
ns = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespaceName,
},
}
By("Create the Namespace for test")
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
metricsTraitBase = v1alpha1.MetricsTrait{
TypeMeta: metav1.TypeMeta{
Kind: metricsTraitKind,
APIVersion: metricsTraitAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
Labels: traitLabel,
},
Spec: v1alpha1.MetricsTraitSpec{
ScrapeService: v1alpha1.ScapeServiceEndPoint{
TargetPort: targetPort,
Path: metricsPath,
Scheme: scheme,
Enabled: pointer.BoolPtr(true),
},
WorkloadReference: runtimev1alpha1.TypedReference{
APIVersion: deploymentAPIVersion,
Kind: deploymentKind,
},
},
}
workloadBase = appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
Labels: deployLabel,
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: deployLabel,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: deployLabel,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container-name",
Image: "alpine",
ImagePullPolicy: corev1.PullNever,
Command: []string{"containerCommand"},
Args: []string{"containerArguments"},
Ports: []corev1.ContainerPort{
{
ContainerPort: int32(podPort),
},
},
},
},
},
},
},
}
})
AfterEach(func() {
// Control-runtime test environment has a bug that can't delete resources like deployment/namespaces
// We have to use different names to segregate between tests
logf.Log.Info("[TEST] Clean up resources after an integration test")
})
It("Test with deployment as workloadBase without selector", func() {
testName := "deploy-without-selector"
By("Create the deployment as the workloadBase")
workload := workloadBase
workload.Name = testName + "-workload"
Expect(k8sClient.Create(ctx, &workload)).ToNot(HaveOccurred())
By("Create the metrics trait pointing to the workloadBase")
metricsTrait := metricsTraitBase
metricsTrait.Name = testName + "-trait"
metricsTrait.Spec.WorkloadReference.Name = workload.Name
Expect(k8sClient.Create(ctx, &metricsTrait)).ToNot(HaveOccurred())
By("Check that we have created the service")
createdService := corev1.Service{}
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: "oam-" + workload.GetName()},
&createdService)
},
time.Second*10, time.Millisecond*500).Should(BeNil())
logf.Log.Info("[TEST] Get the created service", "service ports", createdService.Spec.Ports)
Expect(createdService.GetNamespace()).Should(Equal(namespaceName))
Expect(createdService.Labels).Should(Equal(GetOAMServiceLabel()))
Expect(len(createdService.Spec.Ports)).Should(Equal(1))
Expect(createdService.Spec.Ports[0].Port).Should(BeEquivalentTo(servicePort))
Expect(createdService.Spec.Selector).Should(Equal(deployLabel))
By("Check that we have created the serviceMonitor in the pre-defined namespaceName")
var serviceMonitor monitoringv1.ServiceMonitor
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ServiceMonitorNSName, Name: metricsTrait.GetName()},
&serviceMonitor)
},
time.Second*5, time.Millisecond*50).Should(BeNil())
logf.Log.Info("[TEST] Get the created serviceMonitor", "service end ports", serviceMonitor.Spec.Endpoints)
Expect(serviceMonitor.GetNamespace()).Should(Equal(ServiceMonitorNSName))
Expect(serviceMonitor.Spec.Selector.MatchLabels).Should(Equal(GetOAMServiceLabel()))
Expect(serviceMonitor.Spec.Selector.MatchExpressions).Should(BeNil())
Expect(serviceMonitor.Spec.NamespaceSelector.MatchNames).Should(Equal([]string{metricsTrait.Namespace}))
Expect(serviceMonitor.Spec.NamespaceSelector.Any).Should(BeFalse())
Expect(len(serviceMonitor.Spec.Endpoints)).Should(Equal(1))
Expect(serviceMonitor.Spec.Endpoints[0].Port).Should(BeEmpty())
Expect(*serviceMonitor.Spec.Endpoints[0].TargetPort).Should(BeEquivalentTo(targetPort))
Expect(serviceMonitor.Spec.Endpoints[0].Scheme).Should(Equal(scheme))
Expect(serviceMonitor.Spec.Endpoints[0].Path).Should(Equal(metricsPath))
})
It("Test with deployment as workloadBase selector", func() {
testName := "deploy-with-selector"
By("Create the deployment as the workloadBase")
workload := workloadBase.DeepCopy()
workload.Name = testName + "-workload"
Expect(k8sClient.Create(ctx, workload)).ToNot(HaveOccurred())
By("Create the metrics trait pointing to the workloadBase")
podSelector := map[string]string{"podlabel": "goodboy"}
metricsTrait := metricsTraitBase
metricsTrait.Name = testName + "-trait"
metricsTrait.Spec.WorkloadReference.Name = workload.Name
metricsTrait.Spec.ScrapeService.TargetSelector = podSelector
Expect(k8sClient.Create(ctx, &metricsTrait)).ToNot(HaveOccurred())
By("Check that we have created the service")
createdService := corev1.Service{}
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: "oam-" + workload.GetName()},
&createdService)
},
time.Second*10, time.Millisecond*500).Should(BeNil())
logf.Log.Info("[TEST] Get the created service", "service ports", createdService.Spec.Ports)
Expect(createdService.Labels).Should(Equal(GetOAMServiceLabel()))
Expect(createdService.Spec.Selector).Should(Equal(deployLabel))
By("Check that we have created the serviceMonitor in the pre-defined namespaceName")
var serviceMonitor monitoringv1.ServiceMonitor
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ServiceMonitorNSName, Name: metricsTrait.GetName()},
&serviceMonitor)
},
time.Second*5, time.Millisecond*50).Should(BeNil())
logf.Log.Info("[TEST] Get the created serviceMonitor", "service end ports", serviceMonitor.Spec.Endpoints)
Expect(serviceMonitor.Spec.Selector.MatchLabels).Should(Equal(GetOAMServiceLabel()))
Expect(serviceMonitor.Spec.Selector.MatchExpressions).Should(BeNil())
Expect(serviceMonitor.Spec.NamespaceSelector.MatchNames).Should(Equal([]string{metricsTrait.Namespace}))
Expect(serviceMonitor.Spec.NamespaceSelector.Any).Should(BeFalse())
Expect(len(serviceMonitor.Spec.Endpoints)).Should(Equal(1))
Expect(serviceMonitor.Spec.Endpoints[0].Port).Should(BeEmpty())
Expect(*serviceMonitor.Spec.Endpoints[0].TargetPort).Should(BeEquivalentTo(targetPort))
Expect(serviceMonitor.Spec.Endpoints[0].Scheme).Should(Equal(scheme))
Expect(serviceMonitor.Spec.Endpoints[0].Path).Should(Equal(metricsPath))
})
})

View File

@@ -1,138 +0,0 @@
/*
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 metrics
import (
"context"
"path/filepath"
"testing"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
monitoringv1 "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
oamCore "github.com/oam-dev/kubevela/apis/core.oam.dev"
standardv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
// +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var controllerDone chan struct{}
var serviceMonitorNS corev1.Namespace
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
serviceMonitorNS = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: ServiceMonitorNSName,
},
}
By("Bootstrapping test environment")
useExistCluster := false
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("../../../../..", "charts/vela-core/crds"), // this has all the required CRDs,
},
UseExistingCluster: &useExistCluster,
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
err = standardv1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = monitoringv1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = oamCore.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme
By("Create the k8s client")
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
By("Starting the metrics trait controller in the background")
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
MetricsBindAddress: "0",
Port: 9443,
LeaderElection: false,
LeaderElectionID: "9f6dad5a.oam.dev",
})
Expect(err).ToNot(HaveOccurred())
dm, err := discoverymapper.New(mgr.GetConfig())
Expect(err).ToNot(HaveOccurred())
r := Reconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("MetricsTrait"),
Scheme: mgr.GetScheme(),
dm: dm,
}
Expect(r.SetupWithManager(mgr)).ToNot(HaveOccurred())
controllerDone = make(chan struct{}, 1)
// +kubebuilder:scaffold:builder
go func() {
defer GinkgoRecover()
Expect(mgr.Start(controllerDone)).ToNot(HaveOccurred())
}()
By("Create the serviceMonitor namespace")
Expect(k8sClient.Create(context.Background(), &serviceMonitorNS)).ToNot(HaveOccurred())
close(done)
}, 60)
var _ = AfterSuite(func() {
By("Stop the metricTrait controller")
close(controllerDone)
By("Delete the serviceMonitor namespace")
Expect(k8sClient.Delete(context.Background(), &serviceMonitorNS,
client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed())
By("Tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})

View File

@@ -1,44 +0,0 @@
package ingress
import (
"fmt"
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"k8s.io/api/networking/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
standardv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
)
// TypeNginx is a type of route implementation using [Nginx-Ingress](https://github.com/kubernetes/ingress-nginx)
const TypeNginx = "nginx"
// TypeContour is a type of route implementation using [contour ingress](https://github.com/projectcontour/contour)
const TypeContour = "contour"
const (
// StatusReady represents status is ready
StatusReady = "Ready"
// StatusSynced represents status is synced, this mean the controller has reconciled but not ready
StatusSynced = "Synced"
)
// RouteIngress is an interface of route ingress implementation
type RouteIngress interface {
Construct(routeTrait *standardv1alpha1.Route) []*v1beta1.Ingress
CheckStatus(routeTrait *standardv1alpha1.Route) (string, []runtimev1alpha1.Condition)
}
// GetRouteIngress will get real implementation from type, we could support more in the future.
func GetRouteIngress(provider string, client client.Client) (RouteIngress, error) {
var routeIngress RouteIngress
switch provider {
case TypeNginx, "":
routeIngress = &Nginx{Client: client}
case TypeContour:
routeIngress = &Contour{Client: client}
default:
return nil, fmt.Errorf("unknow route ingress provider '%v', only '%s' is supported now", provider, TypeNginx)
}
return routeIngress, nil
}

View File

@@ -1,16 +0,0 @@
package ingress
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetRouteIngress(t *testing.T) {
_, err := GetRouteIngress("nginx", nil)
assert.NoError(t, err)
_, err = GetRouteIngress("", nil)
assert.NoError(t, err)
_, err = GetRouteIngress("istio", nil)
assert.EqualError(t, err, "unknow route ingress provider 'istio', only 'nginx' is supported now")
}

View File

@@ -1,203 +0,0 @@
package ingress
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
standardv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1"
cmmeta "github.com/wonderflow/cert-manager-api/pkg/apis/meta/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Contour is Contour ingress implementation
type Contour struct {
Client client.Client
}
var _ RouteIngress = &Contour{}
// CheckStatus will check status of the ingress
func (n *Contour) CheckStatus(routeTrait *standardv1alpha1.Route) (string, []runtimev1alpha1.Condition) {
ctx := context.Background()
// check issuer
if routeTrait.Spec.TLS != nil && routeTrait.Spec.TLS.Type != standardv1alpha1.ClusterIssuer {
tls := routeTrait.Spec.TLS
var issuer certmanager.Issuer
err := n.Client.Get(ctx, types.NamespacedName{Namespace: routeTrait.Namespace, Name: tls.IssuerName}, &issuer)
if err != nil || len(issuer.Status.Conditions) < 1 {
var message string
if err == nil {
message = fmt.Sprintf("issuer '%v' is pending to be resolved by controller", tls.IssuerName)
} else {
message = err.Error()
}
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ReasonUnavailable,
Message: message}}
}
// TODO(wonderflow): handle more than one condition case
condition := issuer.Status.Conditions[0]
if condition.Status != cmmeta.ConditionTrue {
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ConditionReason(condition.Reason),
Message: condition.Message}}
}
}
// check ingress
ingresses := n.Construct(routeTrait)
for _, in := range ingresses {
// Check Certificate
if routeTrait.Spec.TLS != nil {
var cert certmanager.Certificate
// check cert
err := n.Client.Get(ctx, types.NamespacedName{Namespace: routeTrait.Namespace, Name: in.Name + "-cert"}, &cert)
if err != nil || len(cert.Status.Conditions) < 1 {
var message string
if err == nil {
message = fmt.Sprintf("CertificateRequest %s is pending to be resolved by controller", in.Name)
} else {
message = err.Error()
}
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ReasonUnavailable,
Message: message}}
}
// TODO(wonderflow): handle more than one condition case
certcondition := cert.Status.Conditions[0]
if certcondition.Status != cmmeta.ConditionTrue || certcondition.Type != certmanager.CertificateConditionReady {
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ConditionReason(certcondition.Reason),
Message: certcondition.Message}}
}
}
// Check Ingress
var ingress v1beta1.Ingress
if err := n.Client.Get(ctx, types.NamespacedName{Namespace: in.Namespace, Name: in.Name}, &ingress); err != nil {
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ReasonUnavailable,
Message: err.Error()}}
}
ingressvalue := ingress.Status.LoadBalancer.Ingress
if len(ingressvalue) < 1 || (ingressvalue[0].IP == "" && ingressvalue[0].Hostname == "") {
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ReasonCreating,
Message: fmt.Sprintf("IP/Hostname of %s ingress is generating", in.Name)}}
}
}
return StatusReady, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeReady, Status: v1.ConditionTrue,
Reason: runtimev1alpha1.ReasonAvailable, LastTransitionTime: metav1.Now()}}
}
// Construct will construct ingress from route
func (*Contour) Construct(routeTrait *standardv1alpha1.Route) []*v1beta1.Ingress {
// Don't create ingress if no host set, this is used for local K8s cluster demo and the route trait will create K8s service only.
if routeTrait.Spec.Host == "" || strings.Contains(routeTrait.Spec.Host, "localhost") || strings.Contains(routeTrait.Spec.Host, "127.0.0.1") {
return nil
}
var ingresses []*v1beta1.Ingress
for idx, rule := range routeTrait.Spec.Rules {
name := rule.Name
if name == "" {
name = strconv.Itoa(idx)
}
backend := rule.Backend
if backend == nil || backend.BackendService == nil {
continue
}
var annotations = make(map[string]string)
annotations["kubernetes.io/ingress.class"] = TypeContour
// SSL
if routeTrait.Spec.TLS != nil {
var issuerAnn = "cert-manager.io/issuer"
if routeTrait.Spec.TLS.Type == standardv1alpha1.ClusterIssuer {
issuerAnn = "cert-manager.io/cluster-issuer"
}
annotations[issuerAnn] = routeTrait.Spec.TLS.IssuerName
}
// todo Rewrite
// todo Custom headers
// todo Send timeout
// Read timeout
if backend.ReadTimeout != 0 {
annotations["projectcontour.io/response-timeout"] = strconv.Itoa(backend.ReadTimeout)
}
ingress := &v1beta1.Ingress{
TypeMeta: metav1.TypeMeta{
Kind: reflect.TypeOf(v1beta1.Ingress{}).Name(),
APIVersion: v1beta1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: routeTrait.Name + "-" + name,
Namespace: routeTrait.Namespace,
Annotations: annotations,
Labels: routeTrait.GetLabels(),
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: routeTrait.GetObjectKind().GroupVersionKind().GroupVersion().String(),
Kind: routeTrait.GetObjectKind().GroupVersionKind().Kind,
UID: routeTrait.GetUID(),
Name: routeTrait.GetName(),
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
}
if routeTrait.Spec.TLS != nil {
ingress.Spec.TLS = []v1beta1.IngressTLS{
{
Hosts: []string{routeTrait.Spec.Host},
SecretName: routeTrait.Name + "-" + name + "-cert",
},
}
}
if rule.DefaultBackend != nil {
ingress.Spec.Backend = &v1beta1.IngressBackend{
Resource: &v1.TypedLocalObjectReference{
APIGroup: &rule.DefaultBackend.APIVersion,
Kind: rule.DefaultBackend.Kind,
Name: rule.DefaultBackend.Name,
},
}
}
ingress.Spec.Rules = []v1beta1.IngressRule{
{
Host: routeTrait.Spec.Host,
IngressRuleValue: v1beta1.IngressRuleValue{HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Path: rule.Path,
Backend: v1beta1.IngressBackend{
ServiceName: backend.BackendService.ServiceName,
ServicePort: backend.BackendService.Port,
},
},
},
}},
},
}
ingresses = append(ingresses, ingress)
}
return ingresses
}

View File

@@ -1,109 +0,0 @@
package ingress
import (
"strconv"
"testing"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
standardv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
)
func TestContourConstruct(t *testing.T) {
tests := map[string]struct {
routeTrait *standardv1alpha1.Route
exp []*v1beta1.Ingress
}{
"normal case": {
routeTrait: &standardv1alpha1.Route{
TypeMeta: metav1.TypeMeta{
Kind: "Route",
APIVersion: "standard.oam.dev/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "trait-test",
},
Spec: standardv1alpha1.RouteSpec{
Host: "test.abc",
TLS: &standardv1alpha1.TLS{
IssuerName: "test-issuer",
Type: "Issuer",
},
Rules: []standardv1alpha1.Rule{
{
Name: "myrule1",
Backend: &standardv1alpha1.Backend{BackendService: &standardv1alpha1.BackendServiceRef{ServiceName: "test", Port: intstr.FromInt(3030)}},
DefaultBackend: &v1alpha1.TypedReference{
APIVersion: "k8s.example.com/v1",
Kind: "StorageBucket",
Name: "static-assets",
},
},
},
},
},
exp: []*v1beta1.Ingress{
{
TypeMeta: metav1.TypeMeta{
Kind: "Ingress",
APIVersion: "networking.k8s.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "trait-test-myrule1",
Annotations: map[string]string{
"kubernetes.io/ingress.class": "contour",
"cert-manager.io/issuer": "test-issuer",
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "standard.oam.dev/v1alpha1",
Kind: "Route",
Name: "trait-test",
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
Spec: v1beta1.IngressSpec{
TLS: []v1beta1.IngressTLS{
{
Hosts: []string{"test.abc"},
SecretName: "trait-test-myrule1-cert",
},
},
Backend: &v1beta1.IngressBackend{Resource: &v1.TypedLocalObjectReference{
APIGroup: pointer.StringPtr("k8s.example.com/v1"),
Kind: "StorageBucket",
Name: "static-assets",
}},
Rules: []v1beta1.IngressRule{
{
Host: "test.abc",
IngressRuleValue: v1beta1.IngressRuleValue{HTTP: &v1beta1.HTTPIngressRuleValue{Paths: []v1beta1.HTTPIngressPath{
{
Path: "",
Backend: v1beta1.IngressBackend{ServiceName: "test", ServicePort: intstr.FromInt(3030)},
},
}}},
},
},
},
},
},
},
}
for message, ti := range tests {
contour := &Contour{}
got := contour.Construct(ti.routeTrait)
assert.Equal(t, len(ti.exp), len(got))
for idx := range ti.exp {
assert.Equal(t, ti.exp[idx], got[idx], message+" index "+strconv.Itoa(idx))
}
}
}

View File

@@ -1,216 +0,0 @@
package ingress
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
standardv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1"
cmmeta "github.com/wonderflow/cert-manager-api/pkg/apis/meta/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Nginx is nginx ingress implementation
type Nginx struct {
Client client.Client
}
var _ RouteIngress = &Nginx{}
// CheckStatus will check status of the ingress
func (n *Nginx) CheckStatus(routeTrait *standardv1alpha1.Route) (string, []runtimev1alpha1.Condition) {
ctx := context.Background()
// check issuer
if routeTrait.Spec.TLS != nil && routeTrait.Spec.TLS.Type != standardv1alpha1.ClusterIssuer {
tls := routeTrait.Spec.TLS
var issuer certmanager.Issuer
err := n.Client.Get(ctx, types.NamespacedName{Namespace: routeTrait.Namespace, Name: tls.IssuerName}, &issuer)
if err != nil || len(issuer.Status.Conditions) < 1 {
var message string
if err == nil {
message = fmt.Sprintf("issuer '%v' is pending to be resolved by controller", tls.IssuerName)
} else {
message = err.Error()
}
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ReasonUnavailable,
Message: message}}
}
// TODO(wonderflow): handle more than one condition case
condition := issuer.Status.Conditions[0]
if condition.Status != cmmeta.ConditionTrue {
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ConditionReason(condition.Reason),
Message: condition.Message}}
}
}
// check ingress
ingresses := n.Construct(routeTrait)
for _, in := range ingresses {
// Check Certificate
if routeTrait.Spec.TLS != nil {
var cert certmanager.Certificate
// check cert
err := n.Client.Get(ctx, types.NamespacedName{Namespace: routeTrait.Namespace, Name: in.Name + "-cert"}, &cert)
if err != nil || len(cert.Status.Conditions) < 1 {
var message string
if err == nil {
message = fmt.Sprintf("CertificateRequest %s is pending to be resolved by controller", in.Name)
} else {
message = err.Error()
}
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ReasonUnavailable,
Message: message}}
}
// TODO(wonderflow): handle more than one condition case
certcondition := cert.Status.Conditions[0]
if certcondition.Status != cmmeta.ConditionTrue || certcondition.Type != certmanager.CertificateConditionReady {
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ConditionReason(certcondition.Reason),
Message: certcondition.Message}}
}
}
// Check Ingress
var ingress v1beta1.Ingress
if err := n.Client.Get(ctx, types.NamespacedName{Namespace: in.Namespace, Name: in.Name}, &ingress); err != nil {
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ReasonUnavailable,
Message: err.Error()}}
}
ingressvalue := ingress.Status.LoadBalancer.Ingress
if len(ingressvalue) < 1 || (ingressvalue[0].IP == "" && ingressvalue[0].Hostname == "") {
return StatusSynced, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeSynced,
Status: v1.ConditionFalse, LastTransitionTime: metav1.Now(), Reason: runtimev1alpha1.ReasonCreating,
Message: fmt.Sprintf("IP/Hostname of %s ingress is generating", in.Name)}}
}
}
return StatusReady, []runtimev1alpha1.Condition{{Type: runtimev1alpha1.TypeReady, Status: v1.ConditionTrue,
Reason: runtimev1alpha1.ReasonAvailable, LastTransitionTime: metav1.Now()}}
}
// Construct will construct ingress from route
func (*Nginx) Construct(routeTrait *standardv1alpha1.Route) []*v1beta1.Ingress {
// Don't create ingress if no host set, this is used for local K8s cluster demo and the route trait will create K8s service only.
if routeTrait.Spec.Host == "" || strings.Contains(routeTrait.Spec.Host, "localhost") || strings.Contains(routeTrait.Spec.Host, "127.0.0.1") {
return nil
}
var ingresses []*v1beta1.Ingress
for idx, rule := range routeTrait.Spec.Rules {
name := rule.Name
if name == "" {
name = strconv.Itoa(idx)
}
backend := rule.Backend
if backend == nil || backend.BackendService == nil {
continue
}
var annotations = make(map[string]string)
annotations["kubernetes.io/ingress.class"] = routeTrait.Spec.IngressClass
// SSL
if routeTrait.Spec.TLS != nil {
var issuerAnn = "cert-manager.io/issuer"
if routeTrait.Spec.TLS.Type == standardv1alpha1.ClusterIssuer {
issuerAnn = "cert-manager.io/cluster-issuer"
}
annotations[issuerAnn] = routeTrait.Spec.TLS.IssuerName
}
// Rewrite
if rule.RewriteTarget != "" {
annotations["nginx.ingress.kubernetes.io/rewrite-target"] = rule.RewriteTarget
}
// Custom headers
var headerSnippet string
for k, v := range rule.CustomHeaders {
headerSnippet += fmt.Sprintf("more_set_headers \"%s: %s\";\n", k, v)
}
if headerSnippet != "" {
annotations["nginx.ingress.kubernetes.io/configuration-snippet"] = headerSnippet
}
// Send timeout
if backend.SendTimeout != 0 {
annotations["nginx.ingress.kubernetes.io/proxy-send-timeout"] = strconv.Itoa(backend.SendTimeout)
}
// Read timeout
if backend.ReadTimeout != 0 {
annotations["nginx.ingress.kubernetes.io/proxyreadtimeout"] = strconv.Itoa(backend.ReadTimeout)
}
ingress := &v1beta1.Ingress{
TypeMeta: metav1.TypeMeta{
Kind: reflect.TypeOf(v1beta1.Ingress{}).Name(),
APIVersion: v1beta1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: routeTrait.Name + "-" + name,
Namespace: routeTrait.Namespace,
Annotations: annotations,
Labels: routeTrait.GetLabels(),
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: routeTrait.GetObjectKind().GroupVersionKind().GroupVersion().String(),
Kind: routeTrait.GetObjectKind().GroupVersionKind().Kind,
UID: routeTrait.GetUID(),
Name: routeTrait.GetName(),
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
}
if routeTrait.Spec.TLS != nil {
ingress.Spec.TLS = []v1beta1.IngressTLS{
{
Hosts: []string{routeTrait.Spec.Host},
SecretName: routeTrait.Name + "-" + name + "-cert",
},
}
}
if rule.DefaultBackend != nil {
ingress.Spec.Backend = &v1beta1.IngressBackend{
Resource: &v1.TypedLocalObjectReference{
APIGroup: &rule.DefaultBackend.APIVersion,
Kind: rule.DefaultBackend.Kind,
Name: rule.DefaultBackend.Name,
},
}
}
ingress.Spec.Rules = []v1beta1.IngressRule{
{
Host: routeTrait.Spec.Host,
IngressRuleValue: v1beta1.IngressRuleValue{HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Path: rule.Path,
Backend: v1beta1.IngressBackend{
ServiceName: backend.BackendService.ServiceName,
ServicePort: backend.BackendService.Port,
},
},
},
}},
},
}
ingresses = append(ingresses, ingress)
}
return ingresses
}

View File

@@ -1,110 +0,0 @@
package ingress
import (
"strconv"
"testing"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
standardv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
)
func TestConstruct(t *testing.T) {
tests := map[string]struct {
routeTrait *standardv1alpha1.Route
exp []*v1beta1.Ingress
}{
"normal case": {
routeTrait: &standardv1alpha1.Route{
TypeMeta: metav1.TypeMeta{
Kind: "Route",
APIVersion: "standard.oam.dev/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "trait-test",
},
Spec: standardv1alpha1.RouteSpec{
Host: "test.abc",
TLS: &standardv1alpha1.TLS{
IssuerName: "test-issuer",
Type: "Issuer",
},
Rules: []standardv1alpha1.Rule{
{
Name: "myrule1",
Backend: &standardv1alpha1.Backend{BackendService: &standardv1alpha1.BackendServiceRef{ServiceName: "test", Port: intstr.FromInt(3030)}},
DefaultBackend: &v1alpha1.TypedReference{
APIVersion: "k8s.example.com/v1",
Kind: "StorageBucket",
Name: "static-assets",
},
},
},
IngressClass: "nginx-private",
},
},
exp: []*v1beta1.Ingress{
{
TypeMeta: metav1.TypeMeta{
Kind: "Ingress",
APIVersion: "networking.k8s.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "trait-test-myrule1",
Annotations: map[string]string{
"kubernetes.io/ingress.class": "nginx-private",
"cert-manager.io/issuer": "test-issuer",
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "standard.oam.dev/v1alpha1",
Kind: "Route",
Name: "trait-test",
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
Spec: v1beta1.IngressSpec{
TLS: []v1beta1.IngressTLS{
{
Hosts: []string{"test.abc"},
SecretName: "trait-test-myrule1-cert",
},
},
Backend: &v1beta1.IngressBackend{Resource: &v1.TypedLocalObjectReference{
APIGroup: pointer.StringPtr("k8s.example.com/v1"),
Kind: "StorageBucket",
Name: "static-assets",
}},
Rules: []v1beta1.IngressRule{
{
Host: "test.abc",
IngressRuleValue: v1beta1.IngressRuleValue{HTTP: &v1beta1.HTTPIngressRuleValue{Paths: []v1beta1.HTTPIngressPath{
{
Path: "",
Backend: v1beta1.IngressBackend{ServiceName: "test", ServicePort: intstr.FromInt(3030)},
},
}}},
},
},
},
},
},
},
}
for message, ti := range tests {
nginx := &Nginx{}
got := nginx.Construct(ti.routeTrait)
assert.Equal(t, len(ti.exp), len(got))
for idx := range ti.exp {
assert.Equal(t, ti.exp[idx], got[idx], message+" index "+strconv.Itoa(idx))
}
}
}

View File

@@ -1,345 +0,0 @@
/*
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 routes
import (
"context"
"encoding/json"
"fmt"
"reflect"
"time"
standardv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/pkg/controller/common"
"github.com/oam-dev/kubevela/pkg/controller/standard.oam.dev/v1alpha1/routes/ingress"
"github.com/oam-dev/kubevela/pkg/controller/utils"
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/go-logr/logr"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/util/retry"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
const (
errApplyNginxIngress = "failed to apply the ingress"
)
var requeueNotReady = 10 * time.Second
// Reconciler reconciles a Route object
type Reconciler struct {
client.Client
dm discoverymapper.DiscoveryMapper
Log logr.Logger
record event.Recorder
Scheme *runtime.Scheme
}
// Reconcile is the main logic of controller
// +kubebuilder:rbac:groups=standard.oam.dev,resources=routes,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=standard.oam.dev,resources=routes/status,verbs=get;update;patch
func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
mLog := r.Log.WithValues("route", req.NamespacedName)
mLog.Info("Reconcile route trait")
// fetch the trait
var routeTrait standardv1alpha1.Route
if err := r.Get(ctx, req.NamespacedName, &routeTrait); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
mLog.Info("Get the route trait",
"host", routeTrait.Spec.Host,
"workload reference", routeTrait.Spec.WorkloadReference,
"labels", routeTrait.GetLabels())
// find the resource object to record the event to, default is the parent appConfig.
eventObj, err := oamutil.LocateParentAppConfig(ctx, r.Client, &routeTrait)
if eventObj == nil {
// fallback to workload itself
mLog.Error(err, "add events to route trait itself", "name", routeTrait.Name)
eventObj = &routeTrait
}
// Fetch the workload instance to which we want to do routes
workload, err := oamutil.FetchWorkload(ctx, r, mLog, &routeTrait)
if err != nil {
mLog.Error(err, "Error while fetching the workload", "workload reference",
routeTrait.GetWorkloadReference())
r.record.Event(eventObj, event.Warning(common.ErrLocatingWorkload, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &routeTrait,
runtimev1alpha1.ReconcileError(errors.Wrap(err, common.ErrLocatingWorkload)))
}
var svc *runtimev1alpha1.TypedReference
if NeedDiscovery(&routeTrait) {
if svc, err = r.discoveryAndFillBackend(ctx, mLog, eventObj, workload, &routeTrait); err != nil {
return oamutil.ReconcileWaitResult, oamutil.PatchCondition(ctx, r, &routeTrait,
runtimev1alpha1.ReconcileError(err))
}
}
routeIngress, err := ingress.GetRouteIngress(routeTrait.Spec.Provider, r.Client)
if err != nil {
mLog.Error(err, "Failed to get routeIngress, use nginx route instead")
routeIngress = &ingress.Nginx{}
}
// Create Ingress
// construct the serviceMonitor that hooks the service to the prometheus server
ingresses := routeIngress.Construct(&routeTrait)
// server side apply the serviceMonitor, only the fields we set are touched
applyOpts := []client.PatchOption{client.ForceOwnership, client.FieldOwner(routeTrait.GetUID())}
for _, ingress := range ingresses {
if err := r.Patch(ctx, ingress, client.Apply, applyOpts...); err != nil {
mLog.Error(err, "Failed to apply to ingress")
r.record.Event(eventObj, event.Warning(errApplyNginxIngress, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &routeTrait,
runtimev1alpha1.ReconcileError(errors.Wrap(err, errApplyNginxIngress)))
}
r.record.Event(eventObj, event.Normal("nginx ingress patched",
fmt.Sprintf("successfully server side patched a route trait `%s`", routeTrait.Name)))
}
// TODO(wonderflow): GC mechanism for no used ingress, service, issuer
var ingressCreated []runtimev1alpha1.TypedReference
for _, ingress := range ingresses {
ingressCreated = append(ingressCreated, runtimev1alpha1.TypedReference{
APIVersion: v1beta1.SchemeGroupVersion.String(),
Kind: reflect.TypeOf(v1beta1.Ingress{}).Name(),
Name: ingress.Name,
UID: routeTrait.UID,
})
}
routeTrait.Status.Ingresses = ingressCreated
routeTrait.Status.Service = svc
var conditions []runtimev1alpha1.Condition
routeTrait.Status.Status, conditions = routeIngress.CheckStatus(&routeTrait)
routeTrait.Status.Conditions = conditions
if routeTrait.Status.Status != ingress.StatusReady {
return ctrl.Result{RequeueAfter: requeueNotReady}, r.UpdateStatus(ctx, &routeTrait)
}
err = r.UpdateStatus(ctx, &routeTrait)
if err != nil {
return oamutil.ReconcileWaitResult, err
}
return ctrl.Result{}, nil
}
// discoveryAndFillBackend will automatically discovery backend for route
func (r *Reconciler) discoveryAndFillBackend(ctx context.Context, mLog logr.Logger, eventObj runtime.Object, workload *unstructured.Unstructured,
routeTrait *standardv1alpha1.Route) (*runtimev1alpha1.TypedReference, error) {
// Fetch the child childResources list from the corresponding workload
childResources, err := oamutil.FetchWorkloadChildResources(ctx, mLog, r, r.dm, workload)
if err != nil {
mLog.Error(err, "Error while fetching the workload child childResources", "workload kind", workload.GetKind(),
"workload name", workload.GetName())
if !apierrors.IsNotFound(err) {
return nil, err
}
}
// try to see if the workload already has services in child childResources, and match for our route
r.fillBackendByCheckChildResource(mLog, routeTrait, childResources)
// Check if still need discovery after childResource filled.
if NeedDiscovery(routeTrait) {
// no service found, we will create service according to rule
svc, err := r.fillBackendByCreatedService(ctx, mLog, workload, routeTrait, childResources)
if err != nil {
r.record.Event(eventObj, event.Warning(common.ErrCreatingService, err))
return nil, errors.Wrap(err, common.ErrCreatingService)
}
r.record.Event(eventObj, event.Normal("Service created",
fmt.Sprintf("successfully automatically created a service `%s`", svc.Name)))
return svc, nil
}
mLog.Info("workload already has service as child resource, will not create service", "workloadName", workload.GetName())
return nil, nil
}
// fillBackendByCreatedService will automatically create service by discovery podTemplate or podSpec.
func (r *Reconciler) fillBackendByCreatedService(ctx context.Context, mLog logr.Logger, workload *unstructured.Unstructured,
routeTrait *standardv1alpha1.Route, childResources []*unstructured.Unstructured) (*runtimev1alpha1.TypedReference, error) {
oamService := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: common.ServiceKind,
APIVersion: common.ServiceAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: routeTrait.GetName(),
Namespace: routeTrait.GetNamespace(),
Labels: utils.SelectOAMAppLabelsWithoutRevision(routeTrait.GetLabels()),
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: routeTrait.GetObjectKind().GroupVersionKind().GroupVersion().String(),
Kind: routeTrait.GetObjectKind().GroupVersionKind().Kind,
UID: routeTrait.GetUID(),
Name: routeTrait.GetName(),
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
},
}
ports, labels, err := DiscoverPortsLabel(ctx, workload, r, r.dm, childResources)
if err != nil {
mLog.Info("[WARN] fail to discovery port and label", "err", err)
return nil, err
}
oamService.Spec.Selector = labels
// use the same port
for _, port := range ports {
oamService.Spec.Ports = append(oamService.Spec.Ports, corev1.ServicePort{
Port: int32(port.IntValue()),
TargetPort: port,
Protocol: corev1.ProtocolTCP,
})
}
// server side apply the service, only the fields we set are touched
applyOpts := []client.PatchOption{client.ForceOwnership, client.FieldOwner(routeTrait.GetUID())}
if err := r.Patch(ctx, oamService, client.Apply, applyOpts...); err != nil {
mLog.Error(err, "Failed to apply to service")
return nil, err
}
FillRouteTraitWithService(oamService, routeTrait)
return &runtimev1alpha1.TypedReference{
APIVersion: common.ServiceAPIVersion,
Kind: common.ServiceKind,
Name: oamService.Name,
UID: routeTrait.UID,
}, nil
}
// UpdateStatus updates standardv1alpha1.Route's Status with retry.RetryOnConflict
func (r *Reconciler) UpdateStatus(ctx context.Context, route *standardv1alpha1.Route, opts ...client.UpdateOption) error {
status := route.DeepCopy().Status
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
if err = r.Get(ctx, types.NamespacedName{Namespace: route.Namespace, Name: route.Name}, route); err != nil {
return
}
route.Status = status
return r.Status().Update(ctx, route, opts...)
})
}
// DiscoverPortsLabel assume the workload or it's childResource will always having spec.template as PodTemplate if discoverable
func DiscoverPortsLabel(ctx context.Context, workload *unstructured.Unstructured, r client.Reader, dm discoverymapper.DiscoveryMapper, childResources []*unstructured.Unstructured) ([]intstr.IntOrString, map[string]string, error) {
// here is the logic follows the design https://github.com/crossplane/oam-kubernetes-runtime/blob/master/design/one-pager-podspecable-workload.md#proposal
// Get WorkloadDefinition
workloadDef, err := oamutil.FetchWorkloadDefinition(ctx, r, dm, workload)
if err != nil {
return nil, nil, err
}
podSpecPath, ok := utils.GetPodSpecPath(workloadDef)
if podSpecPath != "" {
ports, err := utils.DiscoveryFromPodSpec(workload, podSpecPath)
if err != nil {
return nil, nil, err
}
return ports, utils.SelectOAMAppLabelsWithoutRevision(workload.GetLabels()), nil
}
if ok {
return utils.DiscoveryFromPodTemplate(workload, "spec", "template")
}
// If workload is not podSpecable, try to detect it's child resource
var resources = []*unstructured.Unstructured{workload}
resources = append(resources, childResources...)
var gatherErrs []error
for _, w := range resources {
port, labels, err := utils.DiscoveryFromPodTemplate(w, "spec", "template")
if err == nil {
return port, labels, nil
}
gatherErrs = append(gatherErrs, err)
}
return nil, nil, fmt.Errorf("fail to automatically discovery backend from workload %v(%v.%v) and it's child resource, errorList: %v", workload.GetName(), workload.GetAPIVersion(), workload.GetKind(), gatherErrs)
}
// fetch the service that is associated with the workload
func (r *Reconciler) fillBackendByCheckChildResource(mLog logr.Logger,
routeTrait *standardv1alpha1.Route, childResources []*unstructured.Unstructured) {
if len(childResources) == 0 {
return
}
// find the service that has the port
for _, childRes := range childResources {
if childRes.GetAPIVersion() == corev1.SchemeGroupVersion.String() && childRes.GetKind() == reflect.TypeOf(corev1.Service{}).Name() {
data, err := json.Marshal(childRes.Object)
if err != nil {
mLog.Error(err, "error marshal child childResources as K8s Service, continue to check other resource", "resource name", childRes.GetName())
continue
}
var service corev1.Service
err = json.Unmarshal(data, &service)
if err != nil {
mLog.Error(err, "error unmarshal child childResources as K8s Service, continue to check other resource", "resource name", childRes.GetName())
continue
}
FillRouteTraitWithService(&service, routeTrait)
}
}
}
// SetupWithManager setup with manager
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor("Route")).
WithAnnotations("controller", "route")
return ctrl.NewControllerManagedBy(mgr).
For(&standardv1alpha1.Route{}).
Complete(r)
}
// Setup adds a controller that reconciles MetricsTrait.
func Setup(mgr ctrl.Manager) error {
dm, err := discoverymapper.New(mgr.GetConfig())
if err != nil {
return err
}
reconciler := Reconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("Route"),
Scheme: mgr.GetScheme(),
dm: dm,
}
return reconciler.SetupWithManager(mgr)
}

View File

@@ -1,390 +0,0 @@
package routes
import (
"context"
"errors"
"time"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
var _ = Describe("Route Trait Integration Test", func() {
// common var init
ctx := context.Background()
namespaceName := "routetrait-integration-test"
podPort := 8000
issuerName := "my-issuer"
var ns corev1.Namespace
getComponent := func(workloadType, compName string) (v1alpha2.Component, map[string]string, map[string]string) {
podTemplateLabel := map[string]string{"standard.oam.dev": "oam-test-deployment", "workload.oam.dev/type": workloadType}
workloadLabel := map[string]string{"standard.oam.dev": "oam-test-deployment", "app.oam.dev/component": compName, "app.oam.dev/name": "test-app-" + compName}
basedeploy := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Labels: podTemplateLabel,
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: podTemplateLabel,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: podTemplateLabel,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container-name",
Image: "crccheck/hello-world",
ImagePullPolicy: corev1.PullNever,
Ports: []corev1.ContainerPort{
{
ContainerPort: int32(podPort),
}}}}}}},
}
var rp = int32(1)
if workloadType == "webservice" {
basePodSpecc := &v1alpha1.PodSpecWorkload{
TypeMeta: metav1.TypeMeta{
Kind: "PodSpecWorkload",
APIVersion: "standard.oam.dev/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Labels: podTemplateLabel,
},
Spec: v1alpha1.PodSpecWorkloadSpec{
Replicas: &rp,
PodSpec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container-name",
Image: "crccheck/hello-world",
ImagePullPolicy: corev1.PullNever,
Ports: []corev1.ContainerPort{
{
ContainerPort: int32(podPort),
}}}}}},
}
return v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
Kind: "Component",
APIVersion: "core.oam.dev/v1alpha2",
},
ObjectMeta: metav1.ObjectMeta{
Name: compName,
Namespace: ns.Name,
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{Object: basePodSpecc},
},
}, workloadLabel, podTemplateLabel
}
return v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
Kind: "Component",
APIVersion: "core.oam.dev/v1alpha2",
},
ObjectMeta: metav1.ObjectMeta{
Name: compName,
Namespace: ns.Name,
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{Object: basedeploy},
},
}, workloadLabel, podTemplateLabel
}
getAC := func(compName string) v1alpha2.ApplicationConfiguration {
return v1alpha2.ApplicationConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "ApplicationConfiguration",
APIVersion: "core.oam.dev/v1alpha2",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
Name: "test-app-" + compName,
},
Spec: v1alpha2.ApplicationConfigurationSpec{
Components: []v1alpha2.ApplicationConfigurationComponent{{
ComponentName: compName,
Traits: []v1alpha2.ComponentTrait{
{
Trait: runtime.RawExtension{Object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "standard.oam.dev/v1alpha1",
"kind": "Route",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
oam.TraitTypeLabel: "route",
},
},
"spec": map[string]interface{}{
"host": "mycomp.mytest.com",
"tls": map[string]interface{}{
"issuerName": issuerName,
}}}}}}}}}}}
}
BeforeEach(func() {
logf.Log.Info("[TEST] Set up resources before an integration test")
ns = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespaceName,
},
}
By("Create the Namespace for test")
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
By("Create the Issuer for test")
Expect(k8sClient.Create(context.Background(), &certmanager.Issuer{
ObjectMeta: metav1.ObjectMeta{Name: issuerName, Namespace: namespaceName},
Spec: certmanager.IssuerSpec{IssuerConfig: certmanager.IssuerConfig{SelfSigned: &certmanager.SelfSignedIssuer{}}},
})).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
})
AfterEach(func() {
// Control-runtime test environment has a bug that can't delete resources like deployment/namespaces
// We have to use different names to segregate between tests
logf.Log.Info("[TEST] Clean up resources after an integration test")
})
It("Test with child resource no podSpecable but has service child using webservice workload", func() {
compName := "test-webservice"
comp, _, _ := getComponent("webservice", compName)
ac := getAC(compName)
Expect(k8sClient.Create(ctx, &comp)).ToNot(HaveOccurred())
Expect(k8sClient.Create(ctx, &ac)).ToNot(HaveOccurred())
By("Check that we have created the route")
createdRoute := v1alpha1.Route{}
var traitName string
Eventually(
func() error {
err := k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: ac.Name},
&ac)
if err != nil {
return err
}
if len(ac.Status.Workloads) < 1 || len(ac.Status.Workloads[0].Traits) < 1 {
return errors.New("workload or trait not ready")
}
traitName = ac.Status.Workloads[0].Traits[0].Reference.Name
err = k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: traitName},
&createdRoute)
if err != nil {
return err
}
if len(createdRoute.Status.Ingresses) == 0 {
return errors.New("no ingress created")
}
return nil
},
time.Second*30, time.Millisecond*500).Should(BeNil())
By("Check that we have created the ingress")
createdIngress := v1beta1.Ingress{}
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: createdRoute.Status.Ingresses[0].Name},
&createdIngress)
},
time.Second*30, time.Millisecond*500).Should(BeNil())
logf.Log.Info("[TEST] Get the created ingress", "ingress rules", createdIngress.Spec.Rules)
Expect(createdIngress.GetNamespace()).Should(Equal(namespaceName))
Expect(len(createdIngress.Spec.Rules)).Should(Equal(1))
Expect(createdIngress.Spec.Rules[0].Host).Should(Equal("mycomp.mytest.com"))
Expect(createdIngress.Spec.Rules[0].HTTP.Paths[0].Backend.ServiceName).Should(Equal(compName))
Expect(createdIngress.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort.IntVal).Should(Equal(int32(8080)))
})
It("Test with podSpec label with no podSpecPath using deployment workload", func() {
compName := "test-deployment"
comp, _, deploylabel := getComponent("deployment", compName)
ac := getAC(compName)
Expect(k8sClient.Create(ctx, &comp)).ToNot(HaveOccurred())
Expect(k8sClient.Create(ctx, &ac)).ToNot(HaveOccurred())
By("Check that we have created the route")
createdRoute := v1alpha1.Route{}
var traitName string
Eventually(
func() error {
err := k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: ac.Name},
&ac)
if err != nil {
return err
}
if len(ac.Status.Workloads) < 1 || len(ac.Status.Workloads[0].Traits) < 1 {
return errors.New("workload or trait not ready")
}
traitName = ac.Status.Workloads[0].Traits[0].Reference.Name
err = k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: traitName},
&createdRoute)
if err != nil {
return err
}
if len(createdRoute.Status.Ingresses) == 0 {
return errors.New("no ingress created")
}
return nil
},
time.Second*30, time.Millisecond*500).Should(BeNil())
By("Check that we have created the ingress")
createdIngress := v1beta1.Ingress{}
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: createdRoute.Status.Ingresses[0].Name},
&createdIngress)
},
time.Second*30, time.Millisecond*500).Should(BeNil())
logf.Log.Info("[TEST] Get the created ingress", "ingress rules", createdIngress.Spec.Rules)
Expect(createdIngress.GetNamespace()).Should(Equal(namespaceName))
Expect(len(createdIngress.Spec.Rules)).Should(Equal(1))
Expect(createdIngress.Spec.Rules[0].Host).Should(Equal("mycomp.mytest.com"))
Expect(createdIngress.Spec.Rules[0].HTTP.Paths[0].Backend.ServiceName).Should(Equal(traitName))
Expect(createdIngress.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort.IntVal).Should(Equal(int32(8000)))
By("Check that we have created the service")
createdSvc := corev1.Service{}
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: traitName},
&createdSvc)
},
time.Second*30, time.Millisecond*500).Should(BeNil())
logf.Log.Info("[TEST] Get the created service", "service ports", createdSvc.Spec.Ports)
Expect(createdSvc.Spec.Selector).Should(Equal(deploylabel))
Expect(createdSvc.Spec.Ports[0].TargetPort.IntVal).Should(Equal(int32(podPort)))
})
It("Test with podSpecPath specified using deploy workload", func() {
compName := "test-deploy"
comp, _, _ := getComponent("deploy", compName)
ac := getAC(compName)
Expect(k8sClient.Create(ctx, &comp)).ToNot(HaveOccurred())
Expect(k8sClient.Create(ctx, &ac)).ToNot(HaveOccurred())
By("Check that we have created the route")
createdRoute := v1alpha1.Route{}
var traitName string
Eventually(
func() error {
err := k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: ac.Name},
&ac)
if err != nil {
return err
}
if len(ac.Status.Workloads) < 1 || len(ac.Status.Workloads[0].Traits) < 1 {
return errors.New("workload or trait not ready")
}
traitName = ac.Status.Workloads[0].Traits[0].Reference.Name
err = k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: traitName},
&createdRoute)
if err != nil {
return err
}
if len(createdRoute.Status.Ingresses) == 0 {
return errors.New("no ingress created")
}
return nil
},
time.Second*30, time.Millisecond*500).Should(BeNil())
By("Check that we have created the ingress")
createdIngress := v1beta1.Ingress{}
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: createdRoute.Status.Ingresses[0].Name},
&createdIngress)
},
time.Second*30, time.Millisecond*500).Should(BeNil())
logf.Log.Info("[TEST] Get the created ingress", "ingress rules", createdIngress.Spec.Rules)
Expect(createdIngress.GetNamespace()).Should(Equal(namespaceName))
Expect(len(createdIngress.Spec.Rules)).Should(Equal(1))
Expect(createdIngress.Spec.Rules[0].Host).Should(Equal("mycomp.mytest.com"))
Expect(createdIngress.Spec.Rules[0].HTTP.Paths[0].Backend.ServiceName).Should(Equal(traitName))
Expect(createdIngress.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort.IntVal).Should(Equal(int32(8000)))
By("Check that we have created the service")
createdSvc := corev1.Service{}
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: traitName},
&createdSvc)
},
time.Second*30, time.Millisecond*500).Should(BeNil())
logf.Log.Info("[TEST] Get the created service", "service ports", createdSvc.Spec.Ports)
for k, v := range map[string]string{"app.oam.dev/component": compName, "app.oam.dev/name": "test-app-" + compName} {
Expect(createdSvc.Spec.Selector).Should(HaveKeyWithValue(k, v))
}
Expect(createdSvc.Spec.Ports[0].TargetPort.IntVal).Should(Equal(int32(podPort)))
})
It("Test should get error condition if definition not found", func() {
compName := "test-no-def"
comp, _, _ := getComponent("unknow1", compName)
ac := getAC(compName)
Expect(k8sClient.Create(ctx, &comp)).ToNot(HaveOccurred())
Expect(k8sClient.Create(ctx, &ac)).ToNot(HaveOccurred())
By("Check that we have created the route")
createdRoute := v1alpha1.Route{}
var traitName string
Eventually(
func() string {
err := k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: ac.Name},
&ac)
if err != nil {
return err.Error()
}
if len(ac.Status.Workloads) < 1 || len(ac.Status.Workloads[0].Traits) < 1 {
return "workload or trait not ready"
}
traitName = ac.Status.Workloads[0].Traits[0].Reference.Name
err = k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: traitName},
&createdRoute)
if err != nil {
return err.Error()
}
if len(createdRoute.Status.Conditions) == 1 {
return createdRoute.Status.Conditions[0].Message
}
return ""
},
time.Second*10, time.Millisecond*500).Should(Equal(`failed to create the services: WorkloadDefinition.core.oam.dev "unknow1" not found`))
})
})

View File

@@ -1,170 +0,0 @@
/*
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 routes
import (
"context"
"path/filepath"
"testing"
"github.com/crossplane/crossplane-runtime/pkg/logging"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration"
controller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
oamCore "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
standardv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/pkg/controller/standard.oam.dev/v1alpha1/podspecworkload"
// +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var controllerDone chan struct{}
var routeNS corev1.Namespace
var RouteNSName = "route-test"
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
routeNS = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: RouteNSName,
},
}
By("Bootstrapping test environment")
useExistCluster := false
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("../../../../..", "charts/vela-core/crds"), // this has all the required CRDs,
},
UseExistingCluster: &useExistCluster,
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
Expect(standardv1alpha1.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred())
Expect(oamCore.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred())
Expect(certmanager.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme
By("Create the k8s client")
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
By("Starting the route trait controller in the background")
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
Port: 9443,
})
Expect(err).ToNot(HaveOccurred())
r := Reconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("RouteTrait"),
Scheme: mgr.GetScheme(),
}
Expect(r.SetupWithManager(mgr)).ToNot(HaveOccurred())
Expect(applicationconfiguration.Setup(mgr, controller.Args{}, logging.NewLogrLogger(ctrl.Log.WithName("AppConfig")))).ToNot(HaveOccurred())
Expect(podspecworkload.Setup(mgr)).ToNot(HaveOccurred())
controllerDone = make(chan struct{}, 1)
// +kubebuilder:scaffold:builder
go func() {
defer GinkgoRecover()
Expect(mgr.Start(controllerDone)).ToNot(HaveOccurred())
}()
By("Create the routeTrait namespace")
Expect(k8sClient.Create(context.Background(), &routeNS)).ToNot(HaveOccurred())
routeDef := &v1alpha2.TraitDefinition{}
routeDef.Name = "route"
routeDef.Namespace = RouteNSName
routeDef.Spec.Reference.Name = "routes.standard.oam.dev"
routeDef.Spec.WorkloadRefPath = "spec.workloadRef"
Expect(k8sClient.Create(context.Background(), routeDef)).ToNot(HaveOccurred())
webservice := &v1alpha2.WorkloadDefinition{}
webservice.Name = "webservice"
webservice.Namespace = RouteNSName
webservice.Spec.Reference.Name = "deployments.apps"
webservice.Spec.ChildResourceKinds = []v1alpha2.ChildResourceKind{{
APIVersion: "apps/v1",
Kind: "Deployment",
}, {
APIVersion: "v1",
Kind: "Service",
}}
Expect(k8sClient.Create(context.Background(), webservice)).ToNot(HaveOccurred())
deployment := &v1alpha2.WorkloadDefinition{}
deployment.Name = "deployment"
deployment.Namespace = RouteNSName
deployment.Labels = map[string]string{"workload.oam.dev/podspecable": "true"}
deployment.Spec.Reference.Name = "deployments.apps"
Expect(k8sClient.Create(context.Background(), deployment)).ToNot(HaveOccurred())
deploy := &v1alpha2.WorkloadDefinition{}
deploy.Name = "deploy"
deploy.Namespace = RouteNSName
deploy.Spec.PodSpecPath = "spec.template.spec"
deploy.Spec.Reference.Name = "deployments.apps"
Expect(k8sClient.Create(context.Background(), deploy)).ToNot(HaveOccurred())
close(done)
}, 60)
var _ = AfterSuite(func() {
By("Stop the routeTrait controller")
close(controllerDone)
By("Delete the route-test namespace")
Expect(k8sClient.Delete(context.Background(), &routeNS,
client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed())
By("Tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})

View File

@@ -1,72 +0,0 @@
package routes
import (
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
// NeedDiscovery checks the routeTrait Spec if it's needed to automatically discover
func NeedDiscovery(routeTrait *v1alpha1.Route) bool {
if len(routeTrait.Spec.Rules) == 0 {
return true
}
for _, rule := range routeTrait.Spec.Rules {
if rule.Backend == nil {
return true
}
if rule.Backend.BackendService == nil {
return true
}
if rule.Backend.BackendService.ServiceName == "" {
return true
}
}
return false
}
// MatchService try check if the service matches the rules
func MatchService(targetPort intstr.IntOrString, rule v1alpha1.Rule) bool {
// the rule is nil, continue
if rule.Backend == nil || rule.Backend.BackendService == nil || rule.Backend.BackendService.Port.IntValue() == 0 {
return true
}
if rule.Backend.BackendService.ServiceName != "" {
return false
}
// the rule is not null, if any port matches, we regard them are all match
if targetPort == rule.Backend.BackendService.Port {
return true
}
// port is not matched, mark it not match
return false
}
// FillRouteTraitWithService will use existing Service or created Service to fill the spec
func FillRouteTraitWithService(service *corev1.Service, routeTrait *v1alpha1.Route) {
if len(routeTrait.Spec.Rules) == 0 {
routeTrait.Spec.Rules = []v1alpha1.Rule{{Name: "auto-created"}}
}
for idx, rule := range routeTrait.Spec.Rules {
// If backendService.port not specified, will always use the service found and it's first port as backendService.
for _, servicePort := range service.Spec.Ports {
// We use targetPort rather than port to match with the rule, because if serviceName not specified,
// Users will only know containerPort(which is targetPort)
if MatchService(servicePort.TargetPort, rule) {
ref := &v1alpha1.BackendServiceRef{
// Use port of service rather than targetPort, it will be used in ingress pointing to the service
Port: intstr.FromInt(int(servicePort.Port)),
ServiceName: service.Name,
}
if rule.Backend == nil {
rule.Backend = &v1alpha1.Backend{BackendService: ref}
} else {
rule.Backend.BackendService = ref
}
routeTrait.Spec.Rules[idx] = rule
break
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More