diff --git a/.github/workflows/registry.yml b/.github/workflows/registry.yml
index af42620c2..140080392 100644
--- a/.github/workflows/registry.yml
+++ b/.github/workflows/registry.yml
@@ -12,6 +12,7 @@ env:
ENDPOINT: oss-cn-hangzhou.aliyuncs.com
ACCESS_KEY: ${{ secrets.OSS_ACCESS_KEY }}
ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
+ ARTIFACT_HUB_REPOSITORY_ID: ${{ secrets.ARTIFACT_HUB_REPOSITORY_ID }}
jobs:
publish-images:
@@ -63,6 +64,11 @@ jobs:
docker.io/oamdev/vela-core:${{ steps.get_version.outputs.VERSION }}
publish-charts:
+ env:
+ HELM_CHARTS_DIR: charts
+ HELM_CHART: charts/vela-core
+ LEGACY_HELM_CHART: legacy/charts/vela-core-legacy
+ LOCAL_OSS_DIRECTORY: .oss/
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@master
@@ -83,21 +89,34 @@ jobs:
uses: azure/setup-helm@v1
with:
version: v3.4.0
+ - name: Prepare legacy chart
+ run: |
+ rsync -r $LEGACY_HELM_CHART $HELM_CHARTS_DIR
+ rsync -r $HELM_CHART/* $LEGACY_HELM_CHART --exclude=Chart.yaml --exclude=crds
- name: Tag helm chart image
run: |
version=${{ steps.get_version.outputs.VERSION }}
- sed -i "s/latest/$version/g" charts/vela-core/values.yaml
+ sed -i "s/latest/$version/g" $HELM_CHART/values.yaml
+ sed -i "s/latest/$version/g" $LEGACY_HELM_CHART/values.yaml
number=${version#"v"}
- sed -i "s/0.1.0/$number/g" charts/vela-core/Chart.yaml
+ sed -i "s/0.1.0/$number/g" $HELM_CHART/Chart.yaml
+ sed -i "s/0.1.0/$number/g" $LEGACY_HELM_CHART/Chart.yaml
- name: Install ossutil
run: wget http://gosspublic.alicdn.com/ossutil/1.7.0/ossutil64 && chmod +x ossutil64 && mv ossutil64 ossutil
- name: Configure Alibaba Cloud OSSUTIL
run: ./ossutil --config-file .ossutilconfig config -i ${ACCESS_KEY} -k ${ACCESS_KEY_SECRET} -e ${ENDPOINT} -c .ossutilconfig
- name: sync cloud to local
- run: ./ossutil --config-file .ossutilconfig sync oss://kubevelacharts/core .oss/
+ run: ./ossutil --config-file .ossutilconfig sync oss://$BUCKET/core $LOCAL_OSS_DIRECTORY
+ - name: add artifacthub stuff to the repo
+ run: |
+ rsync docs/en/install.md $HELM_CHART/README.md
+ rsync docs/en/install.md $LEGACY_HELM_CHART/README.md
+ sed -i "s/ARTIFACT_HUB_REPOSITORY_ID/$ARTIFACT_HUB_REPOSITORY_ID/g" hack/artifacthub/artifacthub-repo.yml
+ rsync hack/artifacthub/artifacthub-repo.yml $LOCAL_OSS_DIRECTORY
- name: Package helm charts
run: |
- helm package charts/vela-core --destination .oss/
- helm repo index --url https://kubevelacharts.oss-cn-hangzhou.aliyuncs.com/core .oss/
+ helm package $HELM_CHART --destination $LOCAL_OSS_DIRECTORY
+ helm package $LEGACY_HELM_CHART --destination $LOCAL_OSS_DIRECTORY
+ helm repo index --url https://$BUCKET.$ENDPOINT/core $LOCAL_OSS_DIRECTORY
- name: sync local to cloud
- run: ./ossutil --config-file .ossutilconfig sync .oss/ oss://kubevelacharts/core -f
\ No newline at end of file
+ run: ./ossutil --config-file .ossutilconfig sync $LOCAL_OSS_DIRECTORY oss://$BUCKET/core -f
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fcdedb727..44e53a71b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -74,7 +74,7 @@ make core-run
This command will run controller locally, it will use your local KubeConfig which means you need to have a k8s cluster
locally. If you don't have a one, we suggest that you could setup up a cluster with [kind](https://kind.sigs.k8s.io/).
-When you're developing `vela-core`, make sure the controller installed by `vela install` is not running.
+When you're developing `vela-core`, make sure the controller installed by helm chart is not running.
Otherwise, it will conflict with your local running controller.
You can check and uninstall it by using helm.
diff --git a/Makefile b/Makefile
index 2089e4681..8c2cb9422 100644
--- a/Makefile
+++ b/Makefile
@@ -248,7 +248,7 @@ endif
start-dashboard:
go run pkg/server/main/startAPIServer.go &
- cd dashboard && yarn && yarn start && cd ..
+ cd dashboard && npm install && npm start && cd ..
swagger-gen:
$(GOBIN)/swag init -g server/route.go -d pkg/ -o pkg/server/docs/
diff --git a/README.md b/README.md
index c6c85895b..502161406 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@
[](https://github.com/oam-dev/kubevela/releases)
[](https://www.tickgit.com/browse?repo=github.com/oam-dev/kubevela)
[](https://twitter.com/oam_dev)
+[](https://artifacthub.io/packages/search?repo=kubevela)

diff --git a/apis/core.oam.dev/v1alpha2/appdeploy_types.go b/apis/core.oam.dev/v1alpha2/appdeploy_types.go
index 280d02a52..d7671e108 100644
--- a/apis/core.oam.dev/v1alpha2/appdeploy_types.go
+++ b/apis/core.oam.dev/v1alpha2/appdeploy_types.go
@@ -30,7 +30,7 @@ type ApplicationDeploymentSpec struct {
// SourceApplicationName contains the name of the application that we need to upgrade from.
// it can be empty only when it's the first time to deploy the application
- SourceApplicationName string `json:"sourceApplicationName"`
+ SourceApplicationName string `json:"sourceApplicationName,omitempty"`
// The list of component to upgrade in the application.
// We only support single component application so far
@@ -47,6 +47,19 @@ type ApplicationDeploymentSpec struct {
RevertOnDelete *bool `json:"revertOnDelete,omitempty"`
}
+// ApplicationDeploymentStatus defines the observed state of ApplicationDeployment
+type ApplicationDeploymentStatus struct {
+ v1alpha1.RolloutStatus `json:",inline"`
+
+ // LastTargetApplicationName contains the name of the application that we upgraded to
+ // We will restart the rollout if this is not the same as the spec
+ LastTargetApplicationName string `json:"lastTargetApplicationName"`
+
+ // LastSourceApplicationName contains the name of the application that we need to upgrade from.
+ // We will restart the rollout if this is not the same as the spec
+ LastSourceApplicationName string `json:"lastSourceApplicationName,omitempty"`
+}
+
// ApplicationDeployment is the Schema for the ApplicationDeployment API
// +kubebuilder:object:root=true
// +kubebuilder:resource:categories={oam}
@@ -55,8 +68,8 @@ type ApplicationDeployment struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
- Spec ApplicationDeploymentSpec `json:"spec,omitempty"`
- Status v1alpha1.RolloutStatus `json:"status,omitempty"`
+ Spec ApplicationDeploymentSpec `json:"spec,omitempty"`
+ Status ApplicationDeploymentStatus `json:"status,omitempty"`
}
// ApplicationDeploymentList contains a list of ApplicationDeployment
diff --git a/apis/core.oam.dev/v1alpha2/application_types.go b/apis/core.oam.dev/v1alpha2/application_types.go
index 1be6661a6..b2ce60a55 100644
--- a/apis/core.oam.dev/v1alpha2/application_types.go
+++ b/apis/core.oam.dev/v1alpha2/application_types.go
@@ -36,6 +36,8 @@ const (
ApplicationRendering ApplicationPhase = "rendering"
// ApplicationRunning means the app finished rendering and applied result to the cluster
ApplicationRunning ApplicationPhase = "running"
+ // ApplicationHealthChecking means the app finished rendering and applied result to the cluster, but still unhealthy
+ ApplicationHealthChecking ApplicationPhase = "healthChecking"
)
// AppStatus defines the observed state of Application
@@ -49,6 +51,24 @@ type AppStatus struct {
// Components record the related Components created by Application Controller
Components []runtimev1alpha1.TypedReference `json:"components,omitempty"`
+
+ // Services record the status of the application services
+ Services []ApplicationComponentStatus `json:"services,omitempty"`
+}
+
+// ApplicationComponentStatus record the health status of App component
+type ApplicationComponentStatus struct {
+ Name string `json:"name"`
+ Healthy bool `json:"healthy"`
+ Message string `json:"message,omitempty"`
+ Traits []ApplicationTraitStatus `json:"traits,omitempty"`
+}
+
+// ApplicationTraitStatus records the trait health status
+type ApplicationTraitStatus struct {
+ Type string `json:"type"`
+ Healthy bool `json:"healthy"`
+ Message string `json:"message,omitempty"`
}
// ApplicationTrait defines the trait of application
diff --git a/apis/core.oam.dev/v1alpha2/core_types.go b/apis/core.oam.dev/v1alpha2/core_types.go
index c02d880ea..ef560c535 100644
--- a/apis/core.oam.dev/v1alpha2/core_types.go
+++ b/apis/core.oam.dev/v1alpha2/core_types.go
@@ -64,12 +64,36 @@ type WorkloadDefinitionSpec struct {
// +optional
PodSpecPath string `json:"podSpecPath,omitempty"`
+ // Status defines the custom health policy and status message for workload
+ // +optional
+ Status *Status `json:"status,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
+ // +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"`
+
// Extension is used for extension needs by OAM platform builders
// +optional
// +kubebuilder:pruning:PreserveUnknownFields
Extension *runtime.RawExtension `json:"extension,omitempty"`
}
+// Status defines the loop back status of the abstraction by using CUE template
+type Status struct {
+ // CustomStatus defines the custom status message that could display to user
+ // +optional
+ CustomStatus string `json:"customStatus,omitempty"`
+ // HealthPolicy defines the health check policy for the abstraction
+ // +optional
+ HealthPolicy string `json:"healthPolicy,omitempty"`
+}
+
// +kubebuilder:object:root=true
// A WorkloadDefinition registers a kind of Kubernetes custom resource as a
@@ -126,6 +150,20 @@ 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
+ // +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"`
+
+ // Status defines the custom health policy and status message for trait
+ // +optional
+ Status *Status `json:"status,omitempty"`
+
// Extension is used for extension needs by OAM platform builders
// +optional
// +kubebuilder:pruning:PreserveUnknownFields
@@ -401,6 +439,13 @@ 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"`
+
// Reference to a workload created by an ApplicationConfiguration.
Reference runtimev1alpha1.TypedReference `json:"workloadRef,omitempty"`
diff --git a/apis/core.oam.dev/v1alpha2/zz_generated.deepcopy.go b/apis/core.oam.dev/v1alpha2/zz_generated.deepcopy.go
index 3daf780b8..2e198892b 100644
--- a/apis/core.oam.dev/v1alpha2/zz_generated.deepcopy.go
+++ b/apis/core.oam.dev/v1alpha2/zz_generated.deepcopy.go
@@ -34,6 +34,13 @@ func (in *AppStatus) DeepCopyInto(out *AppStatus) {
*out = make([]v1alpha1.TypedReference, len(*in))
copy(*out, *in)
}
+ if in.Services != nil {
+ in, out := &in.Services, &out.Services
+ *out = make([]ApplicationComponentStatus, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppStatus.
@@ -103,6 +110,26 @@ func (in *ApplicationComponent) DeepCopy() *ApplicationComponent {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationComponentStatus) DeepCopyInto(out *ApplicationComponentStatus) {
+ *out = *in
+ if in.Traits != nil {
+ in, out := &in.Traits, &out.Traits
+ *out = make([]ApplicationTraitStatus, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationComponentStatus.
+func (in *ApplicationComponentStatus) DeepCopy() *ApplicationComponentStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(ApplicationComponentStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationConfiguration) DeepCopyInto(out *ApplicationConfiguration) {
*out = *in
@@ -344,6 +371,22 @@ func (in *ApplicationDeploymentSpec) DeepCopy() *ApplicationDeploymentSpec {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationDeploymentStatus) DeepCopyInto(out *ApplicationDeploymentStatus) {
+ *out = *in
+ in.RolloutStatus.DeepCopyInto(&out.RolloutStatus)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDeploymentStatus.
+func (in *ApplicationDeploymentStatus) DeepCopy() *ApplicationDeploymentStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(ApplicationDeploymentStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationList) DeepCopyInto(out *ApplicationList) {
*out = *in
@@ -414,6 +457,21 @@ func (in *ApplicationTrait) DeepCopy() *ApplicationTrait {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationTraitStatus) DeepCopyInto(out *ApplicationTraitStatus) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationTraitStatus.
+func (in *ApplicationTraitStatus) DeepCopy() *ApplicationTraitStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(ApplicationTraitStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CPUResources) DeepCopyInto(out *CPUResources) {
*out = *in
@@ -1607,6 +1665,21 @@ func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Status) DeepCopyInto(out *Status) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status.
+func (in *Status) DeepCopy() *Status {
+ if in == nil {
+ return nil
+ }
+ out := new(Status)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TCPSocketProbe) DeepCopyInto(out *TCPSocketProbe) {
*out = *in
@@ -1694,6 +1767,11 @@ func (in *TraitDefinitionSpec) DeepCopyInto(out *TraitDefinitionSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
+ if in.Status != nil {
+ in, out := &in.Status, &out.Status
+ *out = new(Status)
+ **out = **in
+ }
if in.Extension != nil {
in, out := &in.Extension, &out.Extension
*out = new(runtime.RawExtension)
@@ -1842,6 +1920,11 @@ func (in *WorkloadDefinitionSpec) DeepCopyInto(out *WorkloadDefinitionSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.Status != nil {
+ in, out := &in.Status, &out.Status
+ *out = new(Status)
+ **out = **in
+ }
if in.Extension != nil {
in, out := &in.Extension, &out.Extension
*out = new(runtime.RawExtension)
diff --git a/apis/generate.go b/apis/generate.go
index 555d78324..bad72e0e7 100644
--- a/apis/generate.go
+++ b/apis/generate.go
@@ -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/
+//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
package apis
diff --git a/apis/standard.oam.dev/v1alpha1/rollout_plan_types.go b/apis/standard.oam.dev/v1alpha1/rollout_plan_types.go
index e4abc3838..d30afd82f 100644
--- a/apis/standard.oam.dev/v1alpha1/rollout_plan_types.go
+++ b/apis/standard.oam.dev/v1alpha1/rollout_plan_types.go
@@ -21,7 +21,7 @@ type HookType string
const (
// InitializeRolloutHook execute webhook during the rollout initializing phase
- InitializeRolloutHook HookType = "initilize-rollout"
+ InitializeRolloutHook HookType = "initialize-rollout"
// PreBatchRolloutHook execute webhook before each batch rollout
PreBatchRolloutHook HookType = "pre-batch-rollout"
// PostBatchRolloutHook execute webhook after each batch rollout
@@ -34,38 +34,39 @@ const (
type RollingState string
const (
- // Verifying verify that the rollout setting is valid and the controller can locate both the
+ // VerifyingState verify that the rollout setting is valid and the controller can locate both the
// target and the source
- Verifying RollingState = "verifying"
- // Initializing rollout is initializing all the new resources
- Initializing RollingState = "initializing"
- // Rolling rolling out
- Rolling RollingState = "rolling"
- // Finalising finalize the rolling, possibly clean up the old resources, adjust traffic
- Finalising RollingState = "finalising"
- // Succeed rollout successfully completed to match the desired target state
- Succeed RollingState = "succeed"
- // Failed rollout is failed, the target replica is not reached
+ VerifyingState RollingState = "verifying"
+ // InitializingState rollout is initializing all the new resources
+ InitializingState RollingState = "initializing"
+ // RollingInBatchesState rolling out
+ RollingInBatchesState RollingState = "rollingInBatches"
+ // FinalisingState finalize the rolling, possibly clean up the old resources, adjust traffic
+ FinalisingState RollingState = "finalising"
+ // RolloutSucceedState rollout successfully completed to match the desired target state
+ RolloutSucceedState RollingState = "rolloutSucceed"
+ // RolloutFailedState rollout is failed, the target replica is not reached
// we can not move forward anymore
// we will let the client to decide when or whether to revert
- Failed RollingState = "failed"
+ RolloutFailedState RollingState = "rolloutFailed"
)
// BatchRollingState is the sub state when the rollout is on the fly
type BatchRollingState string
const (
- // BatchRolling still rolling the batch, the batch rolling is not completed yet
- BatchRolling BatchRollingState = "batchRolling"
- // BatchStopped rollout is stopped, the batch rolling is not completed
- BatchStopped BatchRollingState = "batchStopped"
- // BatchReady the pods in the batch are ready. Wait for auto or manual verification.
- BatchReady BatchRollingState = "batchReady"
- // BatchVerifying verifying if the application is ready to roll. This happens when it's either manual or
- // automatic with analysis
- BatchVerifying RollingState = "batchVerifying"
- // BatchAvailable one batch is ready, we could move to the batch
- BatchAvailable BatchRollingState = "batchAvailable"
+ // BatchInitializingState still rolling the batch, the batch rolling is not completed yet
+ BatchInitializingState BatchRollingState = "batchInitializing"
+ // BatchInRollingState still rolling the batch, the batch rolling is not completed yet
+ BatchInRollingState BatchRollingState = "batchInRolling"
+ // BatchVerifyingState verifying if the application is ready to roll.
+ BatchVerifyingState BatchRollingState = "batchVerifying"
+ // BatchRolloutFailedState indicates that the batch didn't get the manual or automatic approval
+ BatchRolloutFailedState BatchRollingState = "batchVerifyFailed"
+ // BatchFinalizingState indicates that all the pods in the are available, we can move on to the next batch
+ BatchFinalizingState BatchRollingState = "batchFinalizing"
+ // BatchReadyState indicates that all the pods in the are upgraded and its state is ready
+ BatchReadyState BatchRollingState = "batchReady"
)
// RolloutPlan fines the details of the rollout plan
@@ -86,7 +87,10 @@ type RolloutPlan struct {
NumBatches *int32 `json:"numBatches,omitempty"`
// The exact distribution among batches.
- // mutually exclusive to NumBatches
+ // mutually exclusive to NumBatches.
+ // The total number cannot exceed the targetSize or the size of the source resource
+ // We will IGNORE the last batch's replica field if it's a percentage since round errors can lead to inaccurate sum
+ // We highly recommend to leave the last batch's replica field empty
// +optional
RolloutBatches []RolloutBatch `json:"rolloutBatches,omitempty"`
@@ -101,7 +105,7 @@ type RolloutPlan struct {
// +optional
Paused bool `json:"paused,omitempty"`
- // RolloutWebhooks provides a way for the rollout to interact with an external process
+ // RolloutWebhooks provide a way for the rollout to interact with an external process
// +optional
RolloutWebhooks []RolloutWebhook `json:"rolloutWebhooks,omitempty"`
@@ -115,6 +119,7 @@ type RolloutPlan struct {
type RolloutBatch struct {
// Replicas is the number of pods to upgrade in this batch
// it can be an absolute number (ex: 5) or a percentage of total pods
+ // we will ignore the percentage of the last batch to just fill the gap
// +optional
// it is mutually exclusive with the PodList field
Replicas intstr.IntOrString `json:"replicas,omitempty"`
@@ -204,16 +209,19 @@ type MetricsExpectedRange struct {
Max *intstr.IntOrString `json:"max,omitempty"`
}
-// RolloutStatus defines the observed state of Rollout
+// RolloutStatus defines the observed state of a rollout plan
type RolloutStatus struct {
// Conditions represents the latest available observations of a CloneSet's current state.
runtimev1alpha1.ConditionedStatus `json:",inline"`
- // The target resource generation
- TargetGeneration string `json:"targetGeneration"`
+ // NewPodTemplateIdentifier is a string that uniquely represent the new pod template
+ // each workload type could use different ways to identify that so we cannot compare between resources
+ NewPodTemplateIdentifier string `json:"targetGeneration,omitempty"`
- // The source resource generation
- SourceGeneration string `json:"sourceGeneration"`
+ // lastAppliedPodTemplateIdentifier is a string that uniquely represent the last pod template
+ // each workload type could use different ways to identify that so we cannot compare between resources
+ // We update this field only after a successful rollout
+ LastAppliedPodTemplateIdentifier string `json:"lastAppliedPodTemplateIdentifier,omitempty"`
// RollingState is the Rollout State
RollingState RollingState `json:"rollingState"`
@@ -223,6 +231,7 @@ type RolloutStatus struct {
BatchRollingState BatchRollingState `json:"batchRollingState"`
// The current batch the rollout is working on/blocked
+ // it starts from 0
CurrentBatch int32 `json:"currentBatch"`
// UpgradedReplicas is the number of Pods upgraded by the rollout controller
diff --git a/apis/standard.oam.dev/v1alpha1/rollout_state.go b/apis/standard.oam.dev/v1alpha1/rollout_state.go
new file mode 100644
index 000000000..9f8f5ad87
--- /dev/null
+++ b/apis/standard.oam.dev/v1alpha1/rollout_state.go
@@ -0,0 +1,298 @@
+package v1alpha1
+
+import (
+ "fmt"
+ "time"
+
+ runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/klog/v2"
+)
+
+// RolloutEvent is used to describe the events during rollout
+type RolloutEvent string
+
+const (
+ // RollingFailedEvent indicates that we encountered an unexpected error during upgrading and can't be retried
+ RollingFailedEvent RolloutEvent = "RollingFailedEvent"
+
+ // RollingRetriableFailureEvent indicates that we encountered an unexpected but retriable error
+ RollingRetriableFailureEvent RolloutEvent = "RollingRetriableFailureEvent"
+
+ // RollingSpecVerifiedEvent indicates that we have successfully verified that the rollout spec
+ RollingSpecVerifiedEvent RolloutEvent = "RollingSpecVerifiedEvent"
+
+ // RollingInitializedEvent indicates that we have finished initializing all the workload resources
+ RollingInitializedEvent RolloutEvent = "RollingInitializedEvent"
+
+ // AllBatchFinishedEvent indicates that all batches are upgraded
+ AllBatchFinishedEvent RolloutEvent = "AllBatchFinishedEvent"
+
+ // RollingFinalizedEvent indicates that we have finalized the rollout which includes but not
+ // limited to the resource garbage collection
+ RollingFinalizedEvent RolloutEvent = "AllBatchFinishedEvent"
+
+ // InitializedOneBatchEvent indicates that we have successfully rolled out one batch
+ InitializedOneBatchEvent RolloutEvent = "InitializedOneBatchEvent"
+
+ // FinishedOneBatchEvent indicates that we have successfully rolled out one batch
+ FinishedOneBatchEvent RolloutEvent = "FinishedOneBatchEvent"
+
+ // BatchRolloutContinueEvent indicates that we need to continue to upgrade the pods in the batch
+ BatchRolloutContinueEvent RolloutEvent = "BatchRolloutContinueEvent"
+
+ // BatchRolloutVerifyingEvent indicates that we are waiting for the approval of resume one batch
+ BatchRolloutVerifyingEvent RolloutEvent = "BatchRolloutVerifyingEvent"
+
+ // OneBatchAvailableEvent indicates that the batch resource is considered available
+ // this events comes after we have examine the pod readiness check and traffic shifting if needed
+ OneBatchAvailableEvent RolloutEvent = "OneBatchAvailable"
+
+ // BatchRolloutApprovedEvent indicates that we got the approval manually
+ BatchRolloutApprovedEvent RolloutEvent = "BatchRolloutApprovedEvent"
+
+ // BatchRolloutFailedEvent indicates that we are waiting for the approval of the
+ BatchRolloutFailedEvent RolloutEvent = "BatchRolloutFailedEvent"
+
+ // WorkloadModifiedEvent indicates that the res
+ WorkloadModifiedEvent RolloutEvent = "WorkloadModifiedEvent"
+)
+
+// These are valid conditions of the rollout.
+const (
+ // RolloutSpecVerified indicates that the rollout spec matches the resource we have in the cluster
+ RolloutSpecVerified runtimev1alpha1.ConditionType = "RolloutSpecVerified"
+ // RolloutInitialized means all the needed initialization work is done
+ RolloutInitialized runtimev1alpha1.ConditionType = "Initialized"
+ // RolloutInProgress means we are upgrading resources.
+ RolloutInProgress runtimev1alpha1.ConditionType = "Ready"
+ // RolloutSucceed means that the rollout is done.
+ RolloutSucceed runtimev1alpha1.ConditionType = "Succeed"
+ // BatchInitialized
+ BatchInitialized runtimev1alpha1.ConditionType = "BatchInitialized"
+ // BatchInRolled
+ BatchInRolled runtimev1alpha1.ConditionType = "BatchInRolled"
+ // BatchVerified
+ BatchVerified runtimev1alpha1.ConditionType = "BatchVerified"
+ // BatchRolloutFailed
+ BatchRolloutFailed runtimev1alpha1.ConditionType = "BatchRolloutFailed"
+ // BatchFinalized
+ BatchFinalized runtimev1alpha1.ConditionType = "BatchFinalized"
+ // BatchReady
+ BatchReady runtimev1alpha1.ConditionType = "BatchReady"
+)
+
+// NewPositiveCondition creates a positive condition type
+func NewPositiveCondition(condType runtimev1alpha1.ConditionType) runtimev1alpha1.Condition {
+ return runtimev1alpha1.Condition{
+ Type: condType,
+ Status: v1.ConditionTrue,
+ LastTransitionTime: metav1.NewTime(time.Now()),
+ }
+}
+
+// NewNegativeCondition creates a false condition type
+func NewNegativeCondition(condType runtimev1alpha1.ConditionType, message string) runtimev1alpha1.Condition {
+ return runtimev1alpha1.Condition{
+ Type: condType,
+ Status: v1.ConditionFalse,
+ LastTransitionTime: metav1.NewTime(time.Now()),
+ Message: message,
+ }
+}
+
+const invalidRollingStateTransition = "the rollout state transition from `%s` state with `%s` is invalid"
+
+const invalidBatchRollingStateTransition = "the batch rolling state transition from `%s` state with `%s` is invalid"
+
+func (r *RolloutStatus) getRolloutConditionType() runtimev1alpha1.ConditionType {
+ // figure out which condition type should we put in the condition depends on its state
+ switch r.RollingState {
+ case VerifyingState:
+ return RolloutSpecVerified
+
+ case InitializingState:
+ return RolloutInitialized
+
+ case RollingInBatchesState:
+ switch r.BatchRollingState {
+ case BatchInitializingState:
+ return BatchInitialized
+
+ case BatchVerifyingState:
+ return BatchVerified
+
+ case BatchFinalizingState:
+ return BatchFinalized
+
+ case BatchReadyState:
+ return BatchReady
+
+ default:
+ return RolloutInProgress
+ }
+
+ case FinalisingState:
+ return RolloutSucceed
+
+ default:
+ return RolloutSucceed
+ }
+}
+
+// RolloutRetry is a special state transition since we need an error message
+func (r *RolloutStatus) RolloutRetry(reason string) {
+ // we can still retry, no change on the state
+ r.SetConditions(NewNegativeCondition(r.getRolloutConditionType(), reason))
+}
+
+// RolloutFailed is a special state transition since we need an error message
+func (r *RolloutStatus) RolloutFailed(reason string) {
+ // set the condition first which depends on the state
+ r.SetConditions(NewNegativeCondition(r.getRolloutConditionType(), reason))
+ r.RollingState = RolloutFailedState
+}
+
+// StateTransition is the center place to do rollout state transition
+// it returns an error if the transition is invalid
+// it changes the coming rollout state if it's valid
+func (r *RolloutStatus) StateTransition(event RolloutEvent) {
+ rollingState := r.RollingState
+ batchRollingState := r.BatchRollingState
+ defer klog.InfoS("try to execute a rollout state transition",
+ "pre rolling state", rollingState,
+ "pre batch rolling state", batchRollingState,
+ "post rolling state", r.RollingState,
+ "post batch rolling state", r.BatchRollingState)
+
+ // we have special transition for these two types of event
+ if event == RollingFailedEvent || event == RollingRetriableFailureEvent {
+ panic(fmt.Errorf(invalidRollingStateTransition, rollingState, event))
+ }
+
+ switch rollingState {
+ case VerifyingState:
+ if event == RollingSpecVerifiedEvent {
+ r.RollingState = InitializingState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ panic(fmt.Errorf(invalidRollingStateTransition, rollingState, event))
+
+ case InitializingState:
+ if event == RollingInitializedEvent {
+ r.RollingState = RollingInBatchesState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ panic(fmt.Errorf(invalidRollingStateTransition, rollingState, event))
+
+ case RollingInBatchesState:
+ r.batchStateTransition(event)
+ return
+
+ case FinalisingState:
+ if event == RollingFinalizedEvent {
+ r.RollingState = RolloutSucceedState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ panic(fmt.Errorf(invalidRollingStateTransition, rollingState, event))
+
+ case RolloutSucceedState:
+ if event == WorkloadModifiedEvent {
+ r.RollingState = VerifyingState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ if event == RollingFinalizedEvent {
+ // no op
+ return
+ }
+ panic(fmt.Errorf(invalidRollingStateTransition, rollingState, event))
+
+ case RolloutFailedState:
+ if event == WorkloadModifiedEvent {
+ r.RollingState = VerifyingState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ if event == RollingFailedEvent {
+ // no op
+ return
+ }
+ panic(fmt.Errorf(invalidRollingStateTransition, rollingState, event))
+
+ default:
+ panic(fmt.Errorf("invalid rolling state %s", rollingState))
+ }
+}
+
+// batchStateTransition handles the state transition when the rollout is in action
+func (r *RolloutStatus) batchStateTransition(event RolloutEvent) {
+ batchRollingState := r.BatchRollingState
+ if event == BatchRolloutFailedEvent {
+ r.BatchRollingState = BatchRolloutFailedState
+ r.RollingState = RolloutFailedState
+ r.SetConditions(NewNegativeCondition(r.getRolloutConditionType(), "failed"))
+ return
+ }
+ switch batchRollingState {
+ case BatchInitializingState:
+ if event == InitializedOneBatchEvent {
+ r.BatchRollingState = BatchInRollingState
+ return
+ }
+ panic(fmt.Errorf(invalidBatchRollingStateTransition, batchRollingState, event))
+
+ case BatchInRollingState:
+ if event == BatchRolloutContinueEvent {
+ // no op
+ return
+ }
+ if event == BatchRolloutVerifyingEvent {
+ r.BatchRollingState = BatchVerifyingState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ panic(fmt.Errorf(invalidBatchRollingStateTransition, batchRollingState, event))
+
+ case BatchVerifyingState:
+ if event == OneBatchAvailableEvent {
+ r.BatchRollingState = BatchFinalizingState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ if event == BatchRolloutVerifyingEvent {
+ // no op
+ return
+ }
+ panic(fmt.Errorf(invalidBatchRollingStateTransition, batchRollingState, event))
+
+ case BatchFinalizingState:
+ if event == FinishedOneBatchEvent {
+ r.BatchRollingState = BatchReadyState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ if event == AllBatchFinishedEvent {
+ // transition out of the batch loop
+ r.RollingState = FinalisingState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ panic(fmt.Errorf(invalidBatchRollingStateTransition, batchRollingState, event))
+
+ case BatchReadyState:
+ if event == BatchRolloutApprovedEvent {
+ r.BatchRollingState = BatchInitializingState
+ r.SetConditions(NewPositiveCondition(r.getRolloutConditionType()))
+ return
+ }
+ panic(fmt.Errorf(invalidBatchRollingStateTransition, batchRollingState, event))
+
+ default:
+ panic(fmt.Errorf("invalid batch rolling state %s", batchRollingState))
+ }
+}
diff --git a/apis/standard.oam.dev/v1alpha1/route_types.go b/apis/standard.oam.dev/v1alpha1/route_types.go
index c06fff06b..802b87ede 100644
--- a/apis/standard.oam.dev/v1alpha1/route_types.go
+++ b/apis/standard.oam.dev/v1alpha1/route_types.go
@@ -43,6 +43,9 @@ type RouteSpec struct {
// 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
diff --git a/apis/types/capability.go b/apis/types/capability.go
index fa89e52a2..3056535c4 100644
--- a/apis/types/capability.go
+++ b/apis/types/capability.go
@@ -18,12 +18,10 @@ package types
import (
"encoding/json"
- "fmt"
"cuelang.org/go/cue"
"github.com/google/go-cmp/cmp"
"github.com/spf13/pflag"
- "k8s.io/apimachinery/pkg/runtime"
)
// Source record the source of Capability
@@ -107,20 +105,6 @@ type Parameter struct {
Alias string `json:"alias,omitempty"`
}
-// ConvertTemplateJSON2Object convert spec.extension to object
-func ConvertTemplateJSON2Object(in *runtime.RawExtension) (Capability, error) {
- var t Capability
- var extension Capability
- if in == nil || in.Raw == nil {
- return t, fmt.Errorf("no template found")
- }
- err := json.Unmarshal(in.Raw, &extension)
- if err == nil {
- t = extension
- }
- return t, err
-}
-
// SetFlagBy set cli flag from Parameter
func SetFlagBy(flags *pflag.FlagSet, v Parameter) {
name := v.Name
diff --git a/charts/vela-core/Chart.yaml b/charts/vela-core/Chart.yaml
index 2b8ea8a43..66e3db5ca 100644
--- a/charts/vela-core/Chart.yaml
+++ b/charts/vela-core/Chart.yaml
@@ -1,6 +1,6 @@
apiVersion: v2
name: vela-core
-description: A Helm chart for Kube Vela core
+description: A Helm chart for KubeVela core
# A chart can be either an 'application' or a 'library' chart.
#
diff --git a/charts/vela-core/crds/core.oam.dev_applicationconfigurations.yaml b/charts/vela-core/crds/core.oam.dev_applicationconfigurations.yaml
index 980e3a7cd..ca5d31a9e 100644
--- a/charts/vela-core/crds/core.oam.dev_applicationconfigurations.yaml
+++ b/charts/vela-core/crds/core.oam.dev_applicationconfigurations.yaml
@@ -386,6 +386,10 @@ spec:
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
scopes:
description: Scopes associated with this workload.
items:
diff --git a/charts/vela-core/crds/core.oam.dev_applicationdeployments.yaml b/charts/vela-core/crds/core.oam.dev_applicationdeployments.yaml
index 625f6b90e..600221c5c 100644
--- a/charts/vela-core/crds/core.oam.dev_applicationdeployments.yaml
+++ b/charts/vela-core/crds/core.oam.dev_applicationdeployments.yaml
@@ -108,7 +108,7 @@ spec:
description: Paused the rollout, default is false
type: boolean
rolloutBatches:
- description: The exact distribution among batches. mutually exclusive to NumBatches
+ description: The exact distribution among batches. mutually exclusive to NumBatches. The total number cannot exceed the targetSize or the size of the source resource We will IGNORE the last batch's replica field if it's a percentage since round errors can lead to inaccurate sum We highly recommend to leave the last batch's replica field empty
items:
description: RolloutBatch is used to describe how the each batch rollout should be
properties:
@@ -210,7 +210,7 @@ spec:
anyOf:
- type: integer
- type: string
- description: 'Replicas is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods it is mutually exclusive with the PodList field'
+ description: 'Replicas is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods we will ignore the percentage of the last batch to just fill the gap it is mutually exclusive with the PodList field'
x-kubernetes-int-or-string: true
type: object
type: array
@@ -218,7 +218,7 @@ spec:
description: RolloutStrategy defines strategies for the rollout plan
type: string
rolloutWebhooks:
- description: RolloutWebhooks provides a way for the rollout to interact with an external process
+ description: RolloutWebhooks provide a way for the rollout to interact with an external process
items:
description: RolloutWebhook holds the reference to external checks used for canary analysis
properties:
@@ -258,11 +258,10 @@ spec:
type: string
required:
- rolloutPlan
- - sourceApplicationName
- targetApplicationName
type: object
status:
- description: RolloutStatus defines the observed state of Rollout
+ description: ApplicationDeploymentStatus defines the observed state of ApplicationDeployment
properties:
batchRollingState:
description: BatchRollingState only meaningful when the Status is rolling
@@ -296,17 +295,23 @@ spec:
type: object
type: array
currentBatch:
- description: The current batch the rollout is working on/blocked
+ description: The current batch the rollout is working on/blocked it starts from 0
format: int32
type: integer
+ lastAppliedPodTemplateIdentifier:
+ description: lastAppliedPodTemplateIdentifier is a string that uniquely represent the last pod template each workload type could use different ways to identify that so we cannot compare between resources We update this field only after a successful rollout
+ type: string
+ lastSourceApplicationName:
+ description: LastSourceApplicationName contains the name of the application that we need to upgrade from. We will restart the rollout if this is not the same as the spec
+ type: string
+ lastTargetApplicationName:
+ description: LastTargetApplicationName contains the name of the application that we upgraded to We will restart the rollout if this is not the same as the spec
+ type: string
rollingState:
description: RollingState is the Rollout State
type: string
- sourceGeneration:
- description: The source resource generation
- type: string
targetGeneration:
- description: The target resource generation
+ description: NewPodTemplateIdentifier is a string that uniquely represent the new pod template each workload type could use different ways to identify that so we cannot compare between resources
type: string
upgradedReadyReplicas:
description: UpgradedReplicas is the number of Pods upgraded by the rollout controller that have a Ready Condition.
@@ -318,9 +323,8 @@ spec:
type: integer
required:
- currentBatch
+ - lastTargetApplicationName
- rollingState
- - sourceGeneration
- - targetGeneration
- upgradedReadyReplicas
- upgradedReplicas
type: object
diff --git a/charts/vela-core/crds/core.oam.dev_applications.yaml b/charts/vela-core/crds/core.oam.dev_applications.yaml
index 460de5c56..2259ac272 100644
--- a/charts/vela-core/crds/core.oam.dev_applications.yaml
+++ b/charts/vela-core/crds/core.oam.dev_applications.yaml
@@ -127,6 +127,37 @@ spec:
- type
type: object
type: array
+ services:
+ description: Services record the status of the application services
+ items:
+ description: ApplicationComponentStatus record the health status of App component
+ properties:
+ healthy:
+ type: boolean
+ message:
+ type: string
+ name:
+ type: string
+ traits:
+ items:
+ description: ApplicationTraitStatus records the trait health status
+ properties:
+ healthy:
+ type: boolean
+ message:
+ type: string
+ type:
+ type: string
+ required:
+ - healthy
+ - type
+ type: object
+ type: array
+ required:
+ - healthy
+ - name
+ type: object
+ type: array
status:
description: ApplicationPhase is a label for the condition of a application at the current time
type: string
diff --git a/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml b/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml
index 9f73a7a7f..ac0ff3ac2 100644
--- a/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml
+++ b/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml
@@ -68,6 +68,22 @@ spec:
revisionEnabled:
description: Revision indicates whether a trait is aware of component revision
type: boolean
+ status:
+ description: Status defines the custom health policy and status message for trait
+ properties:
+ customStatus:
+ description: CustomStatus defines the custom status message that could display to user
+ type: string
+ healthPolicy:
+ 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
diff --git a/charts/vela-core/crds/core.oam.dev_workloaddefinitions.yaml b/charts/vela-core/crds/core.oam.dev_workloaddefinitions.yaml
index 404a5a2eb..15afb139b 100644
--- a/charts/vela-core/crds/core.oam.dev_workloaddefinitions.yaml
+++ b/charts/vela-core/crds/core.oam.dev_workloaddefinitions.yaml
@@ -82,6 +82,22 @@ 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
+ status:
+ description: Status defines the custom health policy and status message for workload
+ properties:
+ customStatus:
+ description: CustomStatus defines the custom status message that could display to user
+ type: string
+ healthPolicy:
+ 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
required:
- definitionRef
type: object
diff --git a/charts/vela-core/crds/standard.oam.dev_rollouts.yaml b/charts/vela-core/crds/standard.oam.dev_rollouts.yaml
deleted file mode 100644
index 7fd23f126..000000000
--- a/charts/vela-core/crds/standard.oam.dev_rollouts.yaml
+++ /dev/null
@@ -1,349 +0,0 @@
-
----
-apiVersion: apiextensions.k8s.io/v1
-kind: CustomResourceDefinition
-metadata:
- annotations:
- controller-gen.kubebuilder.io/version: v0.2.4
- creationTimestamp: null
- name: rollouts.standard.oam.dev
-spec:
- group: standard.oam.dev
- names:
- kind: Rollout
- listKind: RolloutList
- plural: rollouts
- singular: rollout
- scope: Namespaced
- versions:
- - name: v1alpha1
- schema:
- openAPIV3Schema:
- description: Rollout is the Schema for the rollouts 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: RolloutSpec defines the desired state of Rollout
- properties:
- rolloutPlan:
- description: RolloutPlan is the details on how to rollout the resources
- properties:
- canaryMetric:
- description: CanaryMetric provides a way for the rollout process to automatically check certain metrics before complete the process
- items:
- description: CanaryMetric holds the reference to metrics used for canary analysis
- properties:
- interval:
- description: Interval represents the windows size
- type: string
- metricsRange:
- description: Range value accepted for this metric
- properties:
- max:
- anyOf:
- - type: integer
- - type: string
- description: Maximum value
- x-kubernetes-int-or-string: true
- min:
- anyOf:
- - type: integer
- - type: string
- description: Minimum value
- x-kubernetes-int-or-string: true
- type: object
- name:
- description: Name of the metric
- type: string
- templateRef:
- description: TemplateRef references a metric template object
- 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:
- - name
- type: object
- type: array
- lastBatchToRollout:
- description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
- format: int32
- type: integer
- numBatches:
- description: The number of batches, default = 1 mutually exclusive to RolloutBatches
- format: int32
- type: integer
- rolloutBatches:
- description: The exact distribution among batches. mutually exclusive to NumBatches
- items:
- description: RolloutBatch is used to describe how the each batch rollout should be
- properties:
- batchRolloutWebhooks:
- description: RolloutWebhooks provides a way for the batch rollout to interact with an external process
- items:
- description: RolloutWebhook holds the reference to external checks used for canary analysis
- properties:
- metadata:
- additionalProperties:
- type: string
- description: Metadata (key-value pairs) for this webhook
- type: object
- name:
- description: Name of this webhook
- type: string
- timeout:
- description: Request timeout for this webhook
- type: string
- type:
- description: Type of this webhook
- type: string
- url:
- description: URL address of this webhook
- type: string
- required:
- - name
- - type
- - url
- type: object
- type: array
- canaryMetric:
- description: CanaryMetric provides a way for the batch rollout process to automatically check certain metrics before moving to the next batch
- items:
- description: CanaryMetric holds the reference to metrics used for canary analysis
- properties:
- interval:
- description: Interval represents the windows size
- type: string
- metricsRange:
- description: Range value accepted for this metric
- properties:
- max:
- anyOf:
- - type: integer
- - type: string
- description: Maximum value
- x-kubernetes-int-or-string: true
- min:
- anyOf:
- - type: integer
- - type: string
- description: Minimum value
- x-kubernetes-int-or-string: true
- type: object
- name:
- description: Name of the metric
- type: string
- templateRef:
- description: TemplateRef references a metric template object
- 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:
- - name
- type: object
- type: array
- instanceInterval:
- description: The wait time, in seconds, between instances upgrades, default = 0
- format: int32
- type: integer
- maxUnavailable:
- anyOf:
- - type: integer
- - type: string
- description: MaxUnavailable is the max allowed number of pods that is unavailable during the upgrade. We will mark the batch as ready as long as there are less or equal number of pods unavailable than this number. default = 0
- x-kubernetes-int-or-string: true
- podList:
- description: The list of Pods to get upgraded it is mutually exclusive with the Replica field
- items:
- type: string
- type: array
- replica:
- anyOf:
- - type: integer
- - type: string
- description: 'Replica is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods it is mutually exclusive with the PodList field'
- x-kubernetes-int-or-string: true
- type: object
- type: array
- rolloutWebhooks:
- description: RolloutWebhooks provides a way for the rollout to interact with an external process
- items:
- description: RolloutWebhook holds the reference to external checks used for canary analysis
- properties:
- metadata:
- additionalProperties:
- type: string
- description: Metadata (key-value pairs) for this webhook
- type: object
- name:
- description: Name of this webhook
- type: string
- timeout:
- description: Request timeout for this webhook
- type: string
- type:
- description: Type of this webhook
- type: string
- url:
- description: URL address of this webhook
- type: string
- required:
- - name
- - type
- - url
- type: object
- type: array
- stopped:
- description: Stopped the rollout, default is false
- type: boolean
- targetSize:
- description: The size of the target resource. The default is the same as the size of the source resource.
- format: int32
- type: integer
- type: object
- sourceRef:
- description: SourceRef references the source resource that contains the older version of the software. We assume that it's the first time to deploy when we cannot find the source.
- 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
- targetRef:
- description: TargetRef references a target resource that contains the newer version of the software. We assumed that new resource already exists. This is the only resource we work on if the resource is a stateful resource (cloneset/statefulset)
- 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:
- - rolloutPlan
- - targetRef
- type: object
- status:
- description: RolloutStatus defines the observed state of Rollout
- properties:
- batchRollingState:
- description: BatchRollingState only meaningful when the Status is rolling
- type: string
- conditions:
- description: Conditions represents the latest available observations of a CloneSet's current state.
- items:
- description: RolloutCondition is the condition of the rollout
- properties:
- batchRollingState:
- description: BatchRollingState only meaningful when the Status is rolling
- type: string
- lastTransitionTime:
- description: Last time the condition transitioned to this state
- format: date-time
- type: string
- message:
- description: A human readable message indicating details about the transition.
- type: string
- reason:
- description: The reason for the condition's last transition.
- type: string
- rollingState:
- description: RollingState is the Rollout Status
- type: string
- required:
- - rollingState
- type: object
- type: array
- currentBatch:
- description: The current batch the rollout is working on/blocked
- format: int32
- type: integer
- rollingState:
- description: RollingState is the Rollout State
- type: string
- sourceGeneration:
- description: The source resource generation
- type: string
- targetGeneration:
- description: The target resource generation
- type: string
- upgradedReplicas:
- description: UpgradedReplicas is the number of Pods upgraded by the rollout controller that have a Ready Condition.
- format: int32
- type: integer
- required:
- - currentBatch
- - rollingState
- - sourceGeneration
- - targetGeneration
- - upgradedReplicas
- type: object
- type: object
- served: true
- storage: true
- subresources:
- status: {}
-status:
- acceptedNames:
- kind: ""
- plural: ""
- conditions: []
- storedVersions: []
diff --git a/charts/vela-core/crds/standard.oam.dev_rollouttraits.yaml b/charts/vela-core/crds/standard.oam.dev_rollouttraits.yaml
index 00d1ca396..96b9e8f03 100644
--- a/charts/vela-core/crds/standard.oam.dev_rollouttraits.yaml
+++ b/charts/vela-core/crds/standard.oam.dev_rollouttraits.yaml
@@ -98,7 +98,7 @@ spec:
description: Paused the rollout, default is false
type: boolean
rolloutBatches:
- description: The exact distribution among batches. mutually exclusive to NumBatches
+ description: The exact distribution among batches. mutually exclusive to NumBatches. The total number cannot exceed the targetSize or the size of the source resource We will IGNORE the last batch's replica field if it's a percentage since round errors can lead to inaccurate sum We highly recommend to leave the last batch's replica field empty
items:
description: RolloutBatch is used to describe how the each batch rollout should be
properties:
@@ -200,7 +200,7 @@ spec:
anyOf:
- type: integer
- type: string
- description: 'Replicas is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods it is mutually exclusive with the PodList field'
+ description: 'Replicas is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods we will ignore the percentage of the last batch to just fill the gap it is mutually exclusive with the PodList field'
x-kubernetes-int-or-string: true
type: object
type: array
@@ -208,7 +208,7 @@ spec:
description: RolloutStrategy defines strategies for the rollout plan
type: string
rolloutWebhooks:
- description: RolloutWebhooks provides a way for the rollout to interact with an external process
+ description: RolloutWebhooks provide a way for the rollout to interact with an external process
items:
description: RolloutWebhook holds the reference to external checks used for canary analysis
properties:
@@ -288,7 +288,7 @@ spec:
- targetRef
type: object
status:
- description: RolloutStatus defines the observed state of Rollout
+ description: RolloutStatus defines the observed state of a rollout plan
properties:
batchRollingState:
description: BatchRollingState only meaningful when the Status is rolling
@@ -322,17 +322,17 @@ spec:
type: object
type: array
currentBatch:
- description: The current batch the rollout is working on/blocked
+ description: The current batch the rollout is working on/blocked it starts from 0
format: int32
type: integer
+ lastAppliedPodTemplateIdentifier:
+ description: lastAppliedPodTemplateIdentifier is a string that uniquely represent the last pod template each workload type could use different ways to identify that so we cannot compare between resources We update this field only after a successful rollout
+ type: string
rollingState:
description: RollingState is the Rollout State
type: string
- sourceGeneration:
- description: The source resource generation
- type: string
targetGeneration:
- description: The target resource generation
+ description: NewPodTemplateIdentifier is a string that uniquely represent the new pod template each workload type could use different ways to identify that so we cannot compare between resources
type: string
upgradedReadyReplicas:
description: UpgradedReplicas is the number of Pods upgraded by the rollout controller that have a Ready Condition.
@@ -345,8 +345,6 @@ spec:
required:
- currentBatch
- rollingState
- - sourceGeneration
- - targetGeneration
- upgradedReadyReplicas
- upgradedReplicas
type: object
diff --git a/charts/vela-core/crds/standard.oam.dev_routes.yaml b/charts/vela-core/crds/standard.oam.dev_routes.yaml
index 033067044..d9ab0eccc 100644
--- a/charts/vela-core/crds/standard.oam.dev_routes.yaml
+++ b/charts/vela-core/crds/standard.oam.dev_routes.yaml
@@ -37,6 +37,9 @@ spec:
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
diff --git a/charts/vela-core/templates/defwithtemplate/ingress.yaml b/charts/vela-core/templates/defwithtemplate/ingress.yaml
index 51bc22e87..540b88f2d 100644
--- a/charts/vela-core/templates/defwithtemplate/ingress.yaml
+++ b/charts/vela-core/templates/defwithtemplate/ingress.yaml
@@ -7,54 +7,63 @@ metadata:
Please use route trait in cap center for advanced usage."
name: ingress
spec:
+ status:
+ customStatus: |-
+ if len(context.outputs.ingress.status.loadBalancer.ingress) > 0 {
+ message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + context.outputs.ingress.status.loadBalancer.ingress[0].ip
+ }
+ if len(context.outputs.ingress.status.loadBalancer.ingress) == 0 {
+ message: "No loadBalancer found, visiting by using 'vela port-forward " + context.appName + " --route'\n"
+ }
+ healthPolicy: |
+ isHealth: len(context.outputs.service.spec.clusterIP) > 0
appliesToWorkloads:
- webservice
- worker
- extension:
- 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
- }
- },
- ]
- }
- }]
- }
- }
-
+ 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
+ }
+ },
+ ]
+ }
+ }]
+ }
+ }
+
diff --git a/charts/vela-core/templates/defwithtemplate/manualscale.yaml b/charts/vela-core/templates/defwithtemplate/manualscale.yaml
index 2d8c16a44..3fe214c37 100644
--- a/charts/vela-core/templates/defwithtemplate/manualscale.yaml
+++ b/charts/vela-core/templates/defwithtemplate/manualscale.yaml
@@ -12,18 +12,17 @@ spec:
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
- extension:
- template: |-
- output: {
- apiVersion: "core.oam.dev/v1alpha2"
- kind: "ManualScalerTrait"
- spec: {
- replicaCount: parameter.replicas
- }
- }
- parameter: {
- //+short=r
- //+usage=Replicas of the workload
- replicas: *1 | int
- }
-
+ template: |
+ output: {
+ apiVersion: "core.oam.dev/v1alpha2"
+ kind: "ManualScalerTrait"
+ spec: {
+ replicaCount: parameter.replicas
+ }
+ }
+ parameter: {
+ //+short=r
+ //+usage=Replicas of the workload
+ replicas: *1 | int
+ }
+
diff --git a/charts/vela-core/templates/defwithtemplate/task.yaml b/charts/vela-core/templates/defwithtemplate/task.yaml
index 02b32fd36..98f937559 100644
--- a/charts/vela-core/templates/defwithtemplate/task.yaml
+++ b/charts/vela-core/templates/defwithtemplate/task.yaml
@@ -8,40 +8,39 @@ metadata:
spec:
definitionRef:
name: jobs.batch
- extension:
- 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]
- }
-
+ 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]
+ }
+
diff --git a/charts/vela-core/templates/defwithtemplate/webservice.yaml b/charts/vela-core/templates/defwithtemplate/webservice.yaml
index 6830fb7cd..f808db89a 100644
--- a/charts/vela-core/templates/defwithtemplate/webservice.yaml
+++ b/charts/vela-core/templates/defwithtemplate/webservice.yaml
@@ -9,84 +9,83 @@ metadata:
spec:
definitionRef:
name: deployments.apps
- extension:
- 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
- }
-
+ 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
+ }
+
diff --git a/charts/vela-core/templates/defwithtemplate/worker.yaml b/charts/vela-core/templates/defwithtemplate/worker.yaml
index 77e3f6e03..9ddeecb62 100644
--- a/charts/vela-core/templates/defwithtemplate/worker.yaml
+++ b/charts/vela-core/templates/defwithtemplate/worker.yaml
@@ -8,40 +8,39 @@ metadata:
spec:
definitionRef:
name: deployments.apps
- extension:
- 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]
- }
-
+ 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]
+ }
+
diff --git a/charts/vela-core/templates/kubevela-controller.yaml b/charts/vela-core/templates/kubevela-controller.yaml
index ef0f34296..cf5378c7d 100644
--- a/charts/vela-core/templates/kubevela-controller.yaml
+++ b/charts/vela-core/templates/kubevela-controller.yaml
@@ -113,6 +113,7 @@ spec:
- "--webhook-cert-dir={{ .Values.certificate.mountPath }}"
{{ end }}
- "--health-addr=:{{ .Values.healthCheck.port }}"
+ - "--apply-once-only={{ .Values.applyOnceOnly }}"
{{ if ne .Values.disableCaps "" }}
- "--disable-caps={{ .Values.disableCaps }}"
{{ end }}
diff --git a/charts/vela-core/templates/webhook.yaml b/charts/vela-core/templates/webhook.yaml
index f1cc75456..cafa1c831 100644
--- a/charts/vela-core/templates/webhook.yaml
+++ b/charts/vela-core/templates/webhook.yaml
@@ -142,6 +142,28 @@ webhooks:
admissionReviewVersions:
- v1beta1
timeoutSeconds: 5
+ - clientConfig:
+ caBundle: Cg==
+ service:
+ name: {{ template "kubevela.name" . }}-webhook
+ namespace: {{ .Release.Namespace }}
+ path: /validating-core-oam-dev-v1alpha2-traitdefinitions
+ failurePolicy: Fail
+ name: validating.core.oam.dev.v1alpha2.traitdefinitions
+ rules:
+ - apiGroups:
+ - core.oam.dev
+ apiVersions:
+ - v1alpha2
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - traitdefinitions
+ scope: Cluster
+ admissionReviewVersions:
+ - v1beta1
+ timeoutSeconds: 5
- clientConfig:
caBundle: Cg==
service:
diff --git a/charts/vela-core/values.yaml b/charts/vela-core/values.yaml
index b5ebdb3be..a055f1a0f 100644
--- a/charts/vela-core/values.yaml
+++ b/charts/vela-core/values.yaml
@@ -4,6 +4,8 @@
replicaCount: 1
installCertManager: false
+# Valid applyOnceOnly values: true/false/on/off/force
+applyOnceOnly: "off"
useWebhook: true
# By default, don't disable any builtin capabilities
disableCaps: ""
diff --git a/cmd/core/main.go b/cmd/core/main.go
index 78efb6054..8df32937a 100644
--- a/cmd/core/main.go
+++ b/cmd/core/main.go
@@ -9,6 +9,7 @@ import (
"os/signal"
"path/filepath"
"strconv"
+ "strings"
"syscall"
"time"
@@ -18,6 +19,7 @@ import (
injectorcontroller "github.com/oam-dev/trait-injector/controllers"
"github.com/oam-dev/trait-injector/pkg/injector"
"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"
@@ -62,6 +64,7 @@ func init() {
_ = injectorv1alpha1.AddToScheme(scheme)
_ = certmanager.AddToScheme(scheme)
_ = kedav1alpha1.AddToScheme(scheme)
+ _ = kruise.AddToScheme(scheme)
// +kubebuilder:scaffold:scheme
}
@@ -77,6 +80,7 @@ func main() {
var disableCaps string
var storageDriver string
var syncPeriod time.Duration
+ var applyOnceOnly string
flag.BoolVar(&useWebhook, "use-webhook", false, "Enable Admission Webhook")
flag.BoolVar(&useTraitInjector, "use-trait-injector", false, "Enable TraitInjector")
@@ -93,8 +97,8 @@ func main() {
flag.IntVar(&controllerArgs.RevisionLimit, "revision-limit", 50,
"RevisionLimit is the maximum number of revisions that will be maintained. The default value is 50.")
flag.StringVar(&healthAddr, "health-addr", ":9440", "The address the health endpoint binds to.")
- flag.BoolVar(&controllerArgs.ApplyOnceOnly, "apply-once-only", false,
- "For the purpose of some production environment that workload or trait should not be affected if no spec change")
+ flag.StringVar(&applyOnceOnly, "apply-once-only", "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.")
flag.StringVar(&controllerArgs.CustomRevisionHookURL, "custom-revision-hook-url", "",
"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.")
@@ -162,6 +166,23 @@ func main() {
}
}
+ switch strings.ToLower(applyOnceOnly) {
+ case "", "false", string(oamcontroller.ApplyOnceOnlyOff):
+ controllerArgs.ApplyMode = oamcontroller.ApplyOnceOnlyOff
+ setupLog.Info("ApplyOnceOnly is disabled")
+ case "true", string(oamcontroller.ApplyOnceOnlyOn):
+ controllerArgs.ApplyMode = oamcontroller.ApplyOnceOnlyOn
+ setupLog.Info("ApplyOnceOnly is enabled, that means workload or trait only apply once if no spec change even they are changed by others")
+ case string(oamcontroller.ApplyOnceOnlyForce):
+ controllerArgs.ApplyMode = oamcontroller.ApplyOnceOnlyForce
+ setupLog.Info("ApplyOnceOnlyForce is enabled, that means workload or trait only apply once if no spec change even they are changed or deleted by others")
+ default:
+ setupLog.Error(fmt.Errorf("invalid apply-once-only value: %s", applyOnceOnly),
+ "unable to setup the vela core controller",
+ "valid apply-once-only value:", "on/off/force, by default it's off")
+ os.Exit(1)
+ }
+
if err = oamv1alpha2.Setup(mgr, controllerArgs, logging.NewLogrLogger(setupLog)); err != nil {
setupLog.Error(err, "unable to setup the oam core controller")
os.Exit(1)
@@ -201,9 +222,6 @@ func main() {
setupLog.Info("starting the vela controller manager")
- if controllerArgs.ApplyOnceOnly {
- setupLog.Info("applyOnceOnly is enabled that means workload or trait only apply once if no spec change even they are changed by others")
- }
if err := mgr.Start(makeSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
diff --git a/config/samples/app-with-status/app.yaml b/config/samples/app-with-status/app.yaml
new file mode 100644
index 000000000..f2f1917f5
--- /dev/null
+++ b/config/samples/app-with-status/app.yaml
@@ -0,0 +1,21 @@
+apiVersion: core.oam.dev/v1alpha2
+kind: Application
+metadata:
+ name: application-sample
+spec:
+ components:
+ - name: myweb
+ type: worker
+ settings:
+ image: "busybox"
+ cmd:
+ - sleep
+ - "1000"
+ lives: "3"
+ enemies: "alien"
+ traits:
+ - name: ingress
+ properties:
+ domain: "www.example.com"
+ http:
+ "/": 80
\ No newline at end of file
diff --git a/config/samples/app-with-status/template.yaml b/config/samples/app-with-status/template.yaml
new file mode 100644
index 000000000..c32773dcb
--- /dev/null
+++ b/config/samples/app-with-status/template.yaml
@@ -0,0 +1,119 @@
+apiVersion: core.oam.dev/v1alpha2
+kind: WorkloadDefinition
+metadata:
+ name: nworker
+ 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
+ status:
+ healthPolicy: |
+ 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
+ }
+
+ 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
+ }
+ }]
+ }
+ }
+ }
+ }
+
+ 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
+ }
+
+---
+apiVersion: core.oam.dev/v1alpha2
+kind: TraitDefinition
+metadata:
+ name: ingress
+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
+ }
+ },
+ ]
+ }
+ }]
+ }
+ }
diff --git a/dashboard/README.md b/dashboard/README.md
index b47c064f1..d5f31fb8f 100644
--- a/dashboard/README.md
+++ b/dashboard/README.md
@@ -7,37 +7,55 @@ In the root folder of this project, run `make start-dashboard` to start backend
```shell
➜ xxx/src/github.com/oam-dev/kubevela $ make start-dashboard
go run pkg/server/main/startAPIServer.go &
-cd dashboard && yarn && yarn start && cd ..
-yarn install v1.22.4
-warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
-[1/5] 🔍 Validating package.json...
-[2/5] 🔍 Resolving packages...
-success Already up-to-date.
-$ umi g tmp
-✨ Done in 5.89s.
-yarn run v1.22.4
-$ umi dev
-Starting the development server...
-I1230 10:37:54.157092 14236 request.go:621] Throttling request took 1.04915427s, request: GET:https://47.242.145.141:6443/apis/split.smi-spec.io/v1alpha2?timeout=32s
+cd dashboard && npm install && npm start && cd ..
+I0205 11:25:55.742786 5535 request.go:621] Throttling request took 1.002149891s, request: GET:https://47.242.145.141:6443/apis/coordination.k8s.io/v1beta1?timeout=32s
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /api/envs/ --> github.com/oam-dev/kubevela/pkg/server.(*APIServer).CreateEnv-fm (6 handlers)
[GIN-debug] PUT /api/envs/:envName --> github.com/oam-dev/kubevela/pkg/server.(*APIServer).UpdateEnv-fm (6 handlers)
-...
-[GIN-debug] GET /api/version --> github.com/oam-dev/kubevela/pkg/server.(*APIServer).GetVersion-fm (6 handlers)
-[GIN-debug] GET /swagger/*any --> github.com/swaggo/gin-swagger.CustomWrapHandler.func1 (7 handlers)
+[GIN-debug] GET /api/envs/:envName --> github.com/oam-dev/kubevela/pkg/server.(*APIServer).GetEnv-fm (6 handlers)
+[GIN-debug] GET /api/envs/ --> github.com/oam-dev/kubevela/pkg/server.(*APIServer).ListEnv-fm (6 handlers)
+
+> fsevents@1.2.13 install /Users/zhouzhengxi/Programming/golang/src/github.com/oam-dev/kubevela/dashboard/node_modules/watchpack-chokidar2/node_modules/fsevents
+> node install.js
+
+ SOLINK_MODULE(target) Release/.node
+ CXX(target) Release/obj.target/fse/fsevents.o
+ SOLINK_MODULE(target) Release/fse.node
+
+> ejs@2.7.4 postinstall /Users/zhouzhengxi/Programming/golang/src/github.com/oam-dev/kubevela/dashboard/node_modules/umi-webpack-bundle-analyzer/node_modules/ejs
+> node ./postinstall.js
+
+Thank you for installing EJS: built with the Jake JavaScript build tool (https://jakejs.com/)
+
+
+> kubevela@0.0.1 postinstall /Users/zhouzhengxi/Programming/golang/src/github.com/oam-dev/kubevela/dashboard
+> umi g tmp
+
+added 1234 packages from 743 contributors, removed 49 packages, updated 85 packages and audited 3208 packages in 41.551s
+
+235 packages are looking for funding
+ run `npm fund` for details
+
+found 19 vulnerabilities (18 low, 1 high)
+ run `npm audit fix` to fix them, or `npm audit` for details
+
+> kubevela@0.0.1 start /Users/zhouzhengxi/Programming/golang/src/github.com/oam-dev/kubevela/dashboard
+> umi dev
+
+Starting the development server...
✔ Webpack
- Compiled successfully in 26.86s
+ Compiled successfully in 34.81s
- DONE Compiled successfully in 26865ms 10:38:22 AM
+ DONE Compiled successfully in 34815ms 11:27:12 AM
App running at:
- - Local: http://localhost:8000 (copied to clipboard)
- - Network: http://192.168.31.114:8000
+ - Local: http://localhost:8002 (copied to clipboard)
+ - Network: http://30.240.99.101:8002
```
## Development
@@ -45,37 +63,11 @@ I1230 10:37:54.157092 14236 request.go:621] Throttling request took 1.04915427
### Install dependencies
```bash
-yarn
-```
-
-### Build
-
-```bash
-yarn build
+npm install
```
### Start up
```bash
-yarn start
-```
-
-### Lint and Test
-
-- Check code style
-
-```bash
-yarn lint
-```
-
-You can also use script to auto fix some lint error:
-
-```bash
-yarn prettier
-```
-
-- Test code
-
-```bash
-yarn test
+npm start
```
diff --git a/dashboard/config/routes.ts b/dashboard/config/routes.ts
index 3501e39b5..2770ca914 100644
--- a/dashboard/config/routes.ts
+++ b/dashboard/config/routes.ts
@@ -9,6 +9,12 @@
path: `/applications`,
component: './Application',
},
+ /* Application Create should be moved to /Application */
+ {
+ name: 'create_application',
+ path: '/applications/create',
+ component: './CreateApplication'
+ },
{
name: 'capability',
icon: 'AppstoreAddOutlined',
diff --git a/dashboard/package.json b/dashboard/package.json
index d478d2d76..36236f18b 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -54,6 +54,7 @@
"@umijs/route-utils": "^1.0.33",
"antd": "^4.9.4",
"classnames": "^2.2.6",
+ "form-render": "^0.9.0",
"dayjs": "^1.9.7",
"lodash": "^4.17.11",
"moment": "^2.25.3",
@@ -63,6 +64,7 @@
"react-dev-inspector": "^1.1.1",
"react-dom": "^17.0.0",
"react-helmet-async": "^1.0.4",
+ "react-router-dom": "^5.2.0",
"umi": "^3.2.14",
"umi-request": "^1.0.8",
"use-merge-value": "^1.0.1"
@@ -100,7 +102,8 @@
"pro-download": "1.0.1",
"puppeteer-core": "^5.0.0",
"stylelint": "^13.0.0",
- "typescript": "^4.1.2"
+ "typescript": "^4.1.2",
+ "webpack-plugin-fr-theme": "^0.2.0"
},
"engines": {
"node": ">=10.0.0"
diff --git a/dashboard/src/models/useTraitsModel.ts b/dashboard/src/models/useTraitsModel.ts
index c326aefe1..739729ba4 100644
--- a/dashboard/src/models/useTraitsModel.ts
+++ b/dashboard/src/models/useTraitsModel.ts
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
-import * as api from '@/services/traits';
+import * as api from '@/services/capability';
interface State {
loading?: boolean;
diff --git a/dashboard/src/models/useWorkloadsModel.ts b/dashboard/src/models/useWorkloadsModel.ts
index b2b6bf03f..aafa55bea 100644
--- a/dashboard/src/models/useWorkloadsModel.ts
+++ b/dashboard/src/models/useWorkloadsModel.ts
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
-import * as api from '@/services/workloads';
+import * as api from '@/services/capability';
interface State {
loading?: boolean;
diff --git a/dashboard/src/pages/Application/index.tsx b/dashboard/src/pages/Application/index.tsx
index 9c1652d0e..a2b7dda81 100644
--- a/dashboard/src/pages/Application/index.tsx
+++ b/dashboard/src/pages/Application/index.tsx
@@ -6,6 +6,8 @@ import { Link, useModel, useRequest } from 'umi';
import { deleteApplication, getApplications } from '@/services/application';
import { PlusOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-layout';
+// @ts-ignore
+import { Link as ReactLink } from 'react-router-dom';
export default () => {
const { currentEnvironment } = useModel('useEnvironmentModel');
@@ -42,7 +44,7 @@ export default () => {
}>
- Create
+ Create
diff --git a/dashboard/src/pages/CreateApplication/index.tsx b/dashboard/src/pages/CreateApplication/index.tsx
new file mode 100644
index 000000000..75aed7f99
--- /dev/null
+++ b/dashboard/src/pages/CreateApplication/index.tsx
@@ -0,0 +1,174 @@
+import React, { useState } from 'react';
+import { Input, Dropdown, Menu, Button, Divider, Row, Col } from 'antd';
+import { useModel } from '@@/plugin-model/useModel';
+import { DownOutlined, UserOutlined } from '@ant-design/icons';
+import FormRender from 'form-render/lib/antd';
+import { getCapabilityOpenAPISchema } from '@/services/capability';
+// prevent Ant design style from being overridden
+import 'antd/dist/antd.css';
+
+export default (): React.ReactNode => {
+ // @ts-ignore
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { workloadsLoading, workloadList } = useModel('useWorkloadsModel');
+ // @ts-ignore
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { traitsLoading, traitsList } = useModel('useTraitsModel');
+
+ const workloadMenuList = workloadList?.map((i) => (
+ }
+ onClick={() => handleMenuClick('workload_type', i.name)}
+ >
+ {i.name}
+
+ ));
+
+ const traitMenuList = traitsList?.map((i) => (
+ }
+ onClick={() => handleMenuClick('trait', i.name)}
+ >
+ {i.name}
+
+ ));
+
+ const workloadsMenu = {workloadMenuList} ;
+
+ const traitsMenu = {traitMenuList} ;
+
+ // Capability parameters form render
+ const [formData, setData] = useState({});
+ // schema is OpenAPI Schema JSON data
+ const [workloadSchema, setWorkloadSchema] = useState({});
+ const [traitSchema, setTraitSchema] = useState({});
+ const [valid, setValid] = useState([]);
+
+ function handleMenuClick(capabilityType: string, capabilityName: string) {
+ console.log('click', capabilityName);
+ getCapabilityOpenAPISchema(capabilityName).then((result) => {
+ const data = JSON.parse(result.data);
+ if (capabilityType === 'workload_type') {
+ setWorkloadSchema(data);
+ } else if (capabilityType === 'trait') {
+ setTraitSchema(data);
+ }
+ });
+ }
+
+ const onSubmit = () => {
+ // valid == 0: validation passed
+ if (valid.length > 0) {
+ alert(`invalid:${valid.toString()}`);
+ } else {
+ alert(JSON.stringify(formData, null, 2));
+ }
+ };
+
+ return (
+
+
+ Application
+
+
+
+
+ Name:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Services
+
+
+
+
+ Name:
+
+
+
+
+
+
+
+ Type:
+
+
+
+ Select
+
+
+
+
+
+
+ Settings:
+
+
+
+
+
+
+
+
+
+
+
+
+ Traits
+
+
+
+
+ Type:
+
+
+ e.preventDefault()}>
+ Select
+
+
+
+
+
+
+ Properties:
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+
+
+
+ );
+};
diff --git a/dashboard/src/services/capability.ts b/dashboard/src/services/capability.ts
new file mode 100644
index 000000000..f8d4a1ee4
--- /dev/null
+++ b/dashboard/src/services/capability.ts
@@ -0,0 +1,21 @@
+import { request } from 'umi';
+
+/*
+ * workload type list: get /api/workloads/
+ */
+export async function getWorkloads(): Promise> {
+ return request('/api/workloads');
+}
+
+/*
+ * trait list: get /api/traits/
+ */
+export async function getTraits(): Promise> {
+ return request('/api/traits');
+}
+
+export async function getCapabilityOpenAPISchema(
+ capabilityName: string,
+): Promise> {
+ return request(`/api/definitions/${capabilityName}`, { method: 'get' });
+}
diff --git a/dashboard/src/services/traits.ts b/dashboard/src/services/traits.ts
deleted file mode 100644
index 5eb008b1a..000000000
--- a/dashboard/src/services/traits.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { request } from 'umi';
-
-const BASE_PATH = '/api/traits';
-
-/*
- * trait 列表: get /api/traits/
- */
-export async function getTraits(): Promise> {
- return request(BASE_PATH);
-}
diff --git a/dashboard/src/services/workloads.ts b/dashboard/src/services/workloads.ts
deleted file mode 100644
index 2fe0533a2..000000000
--- a/dashboard/src/services/workloads.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { request } from 'umi';
-
-const BASE_PATH = '/api/workloads';
-
-/*
- * workload 列表: get /api/workloads/
- */
-export async function getWorkloads(): Promise> {
- return request(BASE_PATH);
-}
diff --git a/design/vela-core/APIServer-Catalog.md b/design/vela-core/APIServer-Catalog.md
index d5a033b69..a878abc1f 100644
--- a/design/vela-core/APIServer-Catalog.md
+++ b/design/vela-core/APIServer-Catalog.md
@@ -4,14 +4,14 @@
In KubeVela, APIServer provides the RESTful API for external systems (e.g. UI) to manage Vela abstractions like Applications, Definitions; Catalog stores templates to install common-off-the-shell (COTS) capabilities on Kubernetes.
-This doc provides a top-down architecture design for Vela APIServer and Catalog. It clarifies the API interfaces for platform builders to build integration solutions, and describes the architecture design in details for incoming roadmap. Some of the interfaces might have not been implemented yet, but we will follow this design in the future project roadmap.
+This doc provides a top-down architecture design for Vela APIServer and Catalog. It clarifies the API interfaces for platform builders to build integration solutions and describes the architecture design in details for the incoming roadmap. Some of the interfaces might have not been implemented yet, but we will follow this design in the future project roadmap.
## Motivation
This design is based on and tries to resolve the following use cases:
1. UI component wants to discover APIs to integrate with Vela APIServer.
-1. Users want to manage multiple clusters, catalogs, configuration environments in a single place.
+1. Users want to manage multiple clusters, catalogues, configuration environments in a single place.
1. The management data can be stored in a cloud database like MySQL instead of k8s control plane.
1. Because there aren't control logic for those data. This is unlike other Vela resources stored as CR in K8s control plane.
1. It is more expensive to host a k8s control plane than MySQL database on cloud.
@@ -250,7 +250,7 @@ The structure of one package version contains:
- `definitions`: definition files that describe the capabilities from this package to enable on a cluster. Note that these definitions will compared against a cluster on APIServer side to see if a cluster can install or upgrade this package.
-- `conditions/`: definingg conditional checks before deploying this package. For example, check if a CRD with specific version exist, if not then the deployment should fail.
+- `conditions/`: defining conditional checks before deploying this package. For example, check if a CRD with specific version exist, if not then the deployment should fail.
```yaml
# check-crd.yaml
@@ -301,7 +301,7 @@ The structure of one package version contains:
Please refer to `/catalogs/` API endpoint above.
-Under the hood, APIServer will scan the catalog repo based on the predefined structure to parse each packages and versions.
+Under the hood, APIServer will scan the catalog repo based on the predefined structure to parse each packag and versions.
#### Sync a catalog in APIServer
@@ -315,7 +315,7 @@ Vela APIServer aggregates package information from multiple catalog servers. To

-In our future roadmap, we will build a catalog controller for each k8s cluster. Then we will add API endpoint to install the package in APIServer which basically creates a CR to trigger the controller to reconcile package installation into the cluster. We choose this instead of APIServer installing the package because in this way we can bypass the APIServer in the package data transfer path and avoid APIServer becoming single point of failure.
+In our future roadmap, we will build a catalog controller for each k8s cluster. Then we will add API endpoint to install the package in APIServer which basically creates a CR to trigger the controller to reconcile package installation into the cluster. We choose this instead of APIServer installing the package because in this way we can bypass the APIServer in the package data transfer path and avoid APIServer becoming a single point of failure.
## Examples
@@ -323,7 +323,7 @@ In our future roadmap, we will build a catalog controller for each k8s cluster.
### Package parameters
-We can parse the schema of parameters from Helm Chart or Terraform. For example, Helm supports [value schema file](https://www.arthurkoziel.com/validate-helm-chart-values-with-json-schemas/) for input validation and there is an [automation tool](https://github.com/karuppiah7890/helm-schema-gen] to generate the schema.
+We can parse the schema of parameters from Helm Chart or Terraform. For example, Helm supports [value schema file](https://www.arthurkoziel.com/validate-helm-chart-values-with-json-schemas/) for input validation and there is an [automation tool](https://github.com/karuppiah7890/helm-schema-gen) to generate the schema.
### Package dependency
@@ -331,8 +331,8 @@ Instead of having multiple definitions in one package, we could define that one
To provide a bundle of definitions, we could define package dependency. So a parent package could depend on multiple atomic packages to provide a full-fledged capability.
-Package dependency solution will simplify the structure and provide more atomic packages. But this is not a simple problem and beyond the current scope. We will add this on future roadmap.
+Package dependency solution will simplify the structure and provide more atomic packages. But this is not a simple problem and beyond the current scope. We will add this on the future roadmap.
### Multi-tenancy
-For initial version we plan to implement APIServer without multi-tenancy. But as an applicatio platform we expect multi-tenancy is a necessary part of Vela. We will keep API compatibility and might add some sort of auth token (e.g. JWT) as a query parameter in the future.
+For initial version we plan to implement APIServer without multi-tenancy. But as an application platform we expect multi-tenancy is a necessary part of Vela. We will keep API compatibility and might add some sort of auth token (e.g. JWT) as a query parameter in the future.
diff --git a/design/vela-core/apply-once-only.md b/design/vela-core/apply-once-only.md
index 02ba38b87..36f3d55b6 100644
--- a/design/vela-core/apply-once-only.md
+++ b/design/vela-core/apply-once-only.md
@@ -127,3 +127,28 @@ Since discrepancy is found, vela-core controller will apply(update) the Deployme
Thus, the changes we made to the Deployment before will also be eliminated.
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`.
+
+## Usage
+
+Three available options are provided to a vela-core runtime setup flag named `apply-one-only`, referring to three modes:
+
+- off - `apply-once-only` is disabeld, this is the default option
+- on - `apply-once-only` is enabled
+- force - `apply-once-only-force` is enabled
+
+You can set it through `helm` chart value `applyOnceOnly` which is "off" by default if omitted, for example
+
+```shell
+helm install -n vela-system kubevela ./charts/vela-core --set applyOnceOnly=on
+```
+or
+```
+helm install -n vela-system kubevela ./charts/vela-core --set applyOnceOnly=force
+```
+
+
diff --git a/design/vela-core/route.md b/design/vela-core/route.md
index e0ad14aaa..1dd444bb9 100644
--- a/design/vela-core/route.md
+++ b/design/vela-core/route.md
@@ -29,6 +29,9 @@ type RouteSpec struct {
// 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
@@ -92,7 +95,8 @@ Besides `workloadRef`, one Route will have only one `host` and many rules. `host
It's required and will be used to generate mTLS secrets.
Route Trait designed to be compatible with different ingress controller implementations, the `provider` field will allow
-you to give a specified ingress controller type. Currently, only nginx-ingress is supported.
+you to give a specified ingress controller type. The `ingressClass` field will allow you to set the ingressClass.
+Currently, only nginx-ingress is supported.
The `tls` field allow you to specify a TLS for this route with an IssuerName, the IssuerName pointing to an [Issuer Object](https://cert-manager.io/docs/concepts/issuer/)
created by cert-manager. Cert-manager and ingress controller will handle certificate creation and binding.
@@ -148,4 +152,3 @@ route trait will check `WorkloadDefinition` for podSpec field, with the `podSpec
- 2.2 Use ChildResource: If No `PodSpecable` mechanism found in workload, we will continue discovery child resources of workload. If there
is a valid `PodTemplate` structure in child resource, we will regard it as discovery target, use the same strategy like
`workload.oam.dev/podspecable: true` but no `podSpecPath`.
-
diff --git a/docs/en/_sidebar.md b/docs/en/_sidebar.md
index f3d88b8a5..5c95e3656 100644
--- a/docs/en/_sidebar.md
+++ b/docs/en/_sidebar.md
@@ -4,8 +4,7 @@
- [Concepts and Glossaries](/en/concepts.md)
- Platform Team Guide
- - [Overview](/en/platform-engineers/overview.md)
-
+ - [What is KubeVela?](/en/platform-engineers/overview.md)
- Register Capability Modules
- [Workload Type](/en/platform-engineers/workload-type.md)
- [Trait](/en/platform-engineers/trait.md)
diff --git a/docs/en/developers/references/traits/route.md b/docs/en/developers/references/traits/route.md
index 0816a1204..76b36fce1 100644
--- a/docs/en/developers/references/traits/route.md
+++ b/docs/en/developers/references/traits/route.md
@@ -29,7 +29,8 @@ Name | Description | Type | Required | Default
domain | Domain name | string | true | empty
issuer | | string | true | empty
rules | | [[]rules](#rules) | false |
- provider | | string | false |
+ provider | | string | false |
+ ingressClass | | string | false |
### rules
diff --git a/docs/en/introduction.md b/docs/en/introduction.md
index 85c3b9387..2a54466a9 100644
--- a/docs/en/introduction.md
+++ b/docs/en/introduction.md
@@ -8,7 +8,7 @@ The trend of cloud-native technology is moving towards pursuing consistent appli
On the other hand, abstracting Kubernetes to serve developers' requirements is a highly opinionated process, and the resultant abstractions would only make sense had the decision makers been the platform builders. Unfortunately, the platform builders today face the following dilemma:
-*There is no tool or framework for them to easily build user friendly yet highly extensible platforms*.
+*There is no tool or framework for them to easily build user friendly yet highly extensible abstractions*.
Thus, many platforms today are essentially restricted abstractions with in-house add-on mechanisms despite the extensibility of Kubernetes. This makes extending such platforms for developers' requirements or to wider scenarios almost impossible, not to mention taking the full advantage of the rich Kubernetes ecosystems.
diff --git a/docs/en/platform-engineers/overview.md b/docs/en/platform-engineers/overview.md
index 12ecc468d..6fe4e4681 100644
--- a/docs/en/platform-engineers/overview.md
+++ b/docs/en/platform-engineers/overview.md
@@ -1,3 +1,78 @@
-# KubeVela for Platform Builders
+# What is KubeVela?
-TBD: this documentation is still work in progress.
\ No newline at end of file
+This documentation explains "what KubeVela can do for you" in perspective of platform team.
+
+## Overview
+
+KubeVela provides several independent building blocks to help you create application platforms easily.
+
+
+
+### 1. Application Encapsulation
+
+The encapsulation engine enables you to define an `Application` abstraction that encapsulates all the needed resources composed your app.
+
+One typical use case is we want to encapsulate a Kubernetes `Deployment` and a `Service` into a module probably named *Web Service*, and let end users to instantiate this module by simply filling in the parameters (e.g. `image`, `replicas` and `ports`). For example, the [`web-service.ts` ](https://github.com/awslabs/cdk8s/blob/master/examples/typescript/web-service/web-service.ts) lib in cdk8s, the [`kube.cue`](https://github.com/cuelang/cue/blob/b8b489251a3f9ea318830788794c1b4a753031c0/doc/tutorial/kubernetes/quick/services/kube.cue#L70) lib in CUE, and this widely used [Deployment + Service](https://docs.bitnami.com/tutorials/create-your-first-helm-chart/) Helm chart. Of course, some teams with great frontend engineers will choose to build a GUI console to create such abstraction.
+
+The `Application` abstraction supports all the scenarios above. From end user's view, an `Application` is assembled by components (workload specifications) and traits (operational behaviors), for example:
+
+```yaml
+apiVersion: core.oam.dev/v1alpha2
+kind: Application
+metadata:
+ name: application-sample
+spec:
+ components:
+ - name: foo
+ type: worker # component type
+ settings:
+ image: "busybox"
+ cmd:
+ - sleep
+ - "1000"
+ traits:
+ - name: scaler
+ properties:
+ replicas: 10
+ - name: sidecar
+ properties:
+ name: "sidecar-test"
+ image: "nginx"
+ - name: bar
+ type: aliyun-oss # component type
+ bucket: "my-bucket"
+```
+
+In detail, every `component` and `trait` in above abstraction is defined by platform team via `Definition` objects. For example, [`WorkloadDefinition`](https://github.com/oam-dev/kubevela/tree/master/config/samples/application#workload-definition) and [`TraitDefinition`](https://github.com/oam-dev/kubevela/tree/master/config/samples/application#scaler-trait-definition). As the end user, they only need to assemble these modules into an application. Also, if end user has any new requirements, the platform team could customize the template in definitions by any time.
+
+Besides this extensibility, there are several other benefits that the encapsulation engine can bring to you.
+
+#### Unified Abstraction
+
+KubeVela intends to support any possible module types as possible, for example `CUE`, `Terraform`, `Helm`, etc or just a plain Kubernetes CRD. This enables platform team to create unified abstraction that can model and deploy any kind of resource with ease, including cloud services, as long as they could be encapsulated by a supported module type. In the `application-sample` above, it defines a OSS bucket on Alibaba Cloud as a component which is powered by a Terraform module behind the scenes.
+
+#### No Configuration Drift
+
+Many of the existing modules today are defined by client side Infrastructure-as-Code (IaC) tools and even Kubernetes tool like Helm sits at client side as well. So in the nutshell, KubeVela encapsulation engine can just be implemented at client side which would be easier to be adopted.
+
+But client side abstractions, though light-weighted, always lead to an issue called infrastructure/configuration drift, i.e. the generated component instances are not in line with the expected configuration. This could be caused by incomplete coverage, less-than-perfect processes or emergency changes.
+
+In KubeVela, the encapsulation engine is intended to be implemented in a [Kubernetes Control Loop](https://kubernetes.io/docs/concepts/architecture/controller/). This is the key for KubeVela to eliminate the issue of configuration drifting but still keeps the simplicity and software delivery velocity enabled by IaC (and Helm) modules.
+
+#### No "Juggling" Approach to Manage Kubernetes Objects
+
+A typical use case is, as the platform team, we want to leverage `Istio` as the Service Mesh layer to control the traffic to certain `Deployment` instances. But this could be really painful today because we have to enforce end users to define and manage a set of Kubernetes resources in a "juggling" approach. For example, in a simple canary rollout case, the end users have to carefully manage a primary `Deployment`, a primary `Service`, a `root Service`, a canary `Deployment`, a canary `Service`, and have to probably rename the `Deployment` instance after canary promotion (this is actually unacceptable in production because renaming will lead to the app restart). What's worse, we have to expect the users properly set the labels and selectors on those objects carefully because they are the key to ensure proper accessibility of every app instance and the only revision mechanism our Istio controller could count on.
+
+The issue above could be even painful if the workload instance is not `Deployment`, but `StatefulSet` or custom workload type. For example, normally it doesn't make sense to replicate a `StatefulSet` instance during rollout, this means the users have to maintain the name, revision, label, selector, app instances in a totally different approach from `Deployment`.
+
+#### Standard Contract Behind The Abstraction
+
+The encapsulation engine in KubeVela is designed to relieve such burden of managing versionized Kubernetes resources manually. In nutshell, all the needed Kubernetes resources for an app are now encapsulated in a single abstraction, and KubeVela will maintain the instance name, revisions, labels and selector by the battle tested reconcile loop automation, not by human hand. At the meantime, the existence of definition objects allow the platform team to customize the details of all above metadata behind the abstraction, even control the behavior of how to do revision.
+
+Thus, all those metadata now become a standard contract that any day 2 operation controller such as Istio or rollout can rely on. This is the key to ensure our platform could provide user friendly experience but keep "transparent" to the operational behaviors.
+
+### 2. Progressive Rollout
+
+The deployment engine is responsible for progressive rollout of your app following given rollout strategy (e.g. canary, blue-green, etc).
+
+> More information about this section is still work in progress.
diff --git a/docs/en/quick-start.md b/docs/en/quick-start.md
index b1b27acfa..658e709d3 100644
--- a/docs/en/quick-start.md
+++ b/docs/en/quick-start.md
@@ -46,8 +46,7 @@ Services:
Created at: ...
Updated at: ...
Traits:
- - ✅ ingress: domain=testsvc.example.com
- http=map[/:8000]
+ - ✅ ingress: Visiting URL: testsvc.example.com, IP:
```
**In [kind cluster setup](./install.md#kind)**, you can visit the service via localhost. In other setups, replace localhost with ingress address accordingly.
diff --git a/docs/examples/registry/route.yaml b/docs/examples/registry/route.yaml
index 509c7feff..cba56f0ba 100644
--- a/docs/examples/registry/route.yaml
+++ b/docs/examples/registry/route.yaml
@@ -35,7 +35,8 @@ spec:
rules: parameter.rules
}
- provider: *"nginx" | parameter.provider
+ provider: *"nginx" | parameter.provider
+ ingressClass: *"nginx" | parameter.ingressClass
}
}
parameter: {
@@ -47,6 +48,7 @@ spec:
path: string
rewriteTarget: *"" | string
}]
- provider?: string
+ provider?: string
+ ingressClass?: string
}
diff --git a/docs/resources/kubevela-runtime.png b/docs/resources/kubevela-runtime.png
new file mode 100644
index 000000000..a91b94ed2
Binary files /dev/null and b/docs/resources/kubevela-runtime.png differ
diff --git a/e2e/capability/capability_test.go b/e2e/capability/capability_test.go
index 2196c19f7..1fc68a7b3 100644
--- a/e2e/capability/capability_test.go
+++ b/e2e/capability/capability_test.go
@@ -17,6 +17,11 @@ var (
URL: "https://github.com/oam-dev/kubevela/tree/master/pkg/plugins/testdata",
}
+ websvcCapability = types.Capability{
+ Name: "webservice.testapps",
+ Type: types.TypeWorkload,
+ }
+
scaleCapability = types.Capability{
Name: "scaler",
Type: types.TypeTrait,
@@ -41,9 +46,8 @@ var _ = ginkgo.Describe("Capability", func() {
cli := fmt.Sprintf("vela cap center config %s %s", capabilityCenterBasic.Name, capabilityCenterBasic.URL)
output, err := e2e.Exec(cli)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
- expectedOutput1 := fmt.Sprintf("Successfully configured capability center: %s, start to sync from remote", capabilityCenterBasic.Name)
+ expectedOutput1 := fmt.Sprintf("Successfully configured capability center %s and sync from remote", capabilityCenterBasic.Name)
gomega.Expect(output).To(gomega.ContainSubstring(expectedOutput1))
- gomega.Expect(output).To(gomega.ContainSubstring("sync finished"))
})
ginkgo.It("list capability centers", func() {
@@ -58,8 +62,18 @@ var _ = ginkgo.Describe("Capability", func() {
})
ginkgo.Context("capability", func() {
- ginkgo.It("install a capability to cluster", func() {
- cli := fmt.Sprintf("vela cap add %s/%s", capabilityCenterBasic.Name, scaleCapability.Name)
+ ginkgo.It("install a workload capability to cluster", func() {
+ cli := fmt.Sprintf("vela cap install %s/%s", capabilityCenterBasic.Name, websvcCapability.Name)
+ output, err := e2e.Exec(cli)
+ gomega.Expect(err).NotTo(gomega.HaveOccurred())
+ expectedSubStr1 := fmt.Sprintf("Installing %s capability", websvcCapability.Type)
+ expectedSubStr2 := fmt.Sprintf("Successfully installed capability %s from %s", websvcCapability.Name, capabilityCenterBasic.Name)
+ gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr1))
+ gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr2))
+ })
+
+ ginkgo.It("install a trait capability to cluster", func() {
+ cli := fmt.Sprintf("vela cap install %s/%s", capabilityCenterBasic.Name, scaleCapability.Name)
output, err := e2e.Exec(cli)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
expectedSubStr1 := fmt.Sprintf("Installing %s capability", scaleCapability.Type)
@@ -68,8 +82,8 @@ var _ = ginkgo.Describe("Capability", func() {
gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr2))
})
- ginkgo.It("install a trait without definition reference to cluster", func() {
- cli := fmt.Sprintf("vela cap add %s/%s", capabilityCenterBasic.Name, ingressCapability.Name)
+ ginkgo.It("install a trait capability without definition reference to cluster", func() {
+ cli := fmt.Sprintf("vela cap install %s/%s", capabilityCenterBasic.Name, ingressCapability.Name)
output, err := e2e.Exec(cli)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
expectedSubStr1 := fmt.Sprintf("Installing %s capability", ingressCapability.Type)
@@ -84,11 +98,34 @@ var _ = ginkgo.Describe("Capability", func() {
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(output).To(gomega.ContainSubstring("NAME"))
gomega.Expect(output).To(gomega.ContainSubstring("CENTER"))
+ gomega.Expect(output).To(gomega.ContainSubstring(websvcCapability.Name))
+ gomega.Expect(output).To(gomega.ContainSubstring(ingressCapability.Name))
gomega.Expect(output).To(gomega.ContainSubstring(scaleCapability.Name))
gomega.Expect(output).To(gomega.ContainSubstring(routeCapability.Name))
gomega.Expect(output).To(gomega.ContainSubstring("installed"))
})
+ ginkgo.It("uninstall a workload capability from cluster", func() {
+ cli := fmt.Sprintf("vela cap uninstall %s", websvcCapability.Name)
+ output, err := e2e.Exec(cli)
+ gomega.Expect(err).NotTo(gomega.HaveOccurred())
+ expectedSubStr := fmt.Sprintf("Successfully uninstalled capability %s", websvcCapability.Name)
+ gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr))
+ })
+
+ ginkgo.It("uninstall a trait capability from cluster", func() {
+ cli := fmt.Sprintf("vela cap uninstall %s", ingressCapability.Name)
+ output, err := e2e.Exec(cli)
+ gomega.Expect(err).NotTo(gomega.HaveOccurred())
+ expectedSubStr := fmt.Sprintf("Successfully uninstalled capability %s", ingressCapability.Name)
+ gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr))
+
+ // unstall other installed test capability
+ cli = fmt.Sprintf("vela cap uninstall %s", scaleCapability.Name)
+ _, err = e2e.Exec(cli)
+ gomega.Expect(err).NotTo(gomega.HaveOccurred())
+ })
+
ginkgo.It("delete a capability center", func() {
cli := fmt.Sprintf("vela cap center remove %s", capabilityCenterBasic.Name)
output, err := e2e.Exec(cli)
diff --git a/go.mod b/go.mod
index 0fc91b7e0..8d355d5f8 100644
--- a/go.mod
+++ b/go.mod
@@ -37,6 +37,7 @@ require (
github.com/olekukonko/tablewriter v0.0.2
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
@@ -79,7 +80,6 @@ require (
replace (
github.com/Azure/go-autorest => github.com/Azure/go-autorest v12.2.0+incompatible // https://github.com/kubernetes/client-go/issues/628
- github.com/Sirupsen/logrus v1.7.0 => github.com/sirupsen/logrus v1.7.0
// fix build issue https://github.com/docker/distribution/issues/2406
github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d
github.com/docker/docker => github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
diff --git a/go.sum b/go.sum
index ddcb41f69..149af76b0 100644
--- a/go.sum
+++ b/go.sum
@@ -390,7 +390,6 @@ github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkE
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/etcd v3.3.15+incompatible h1:+9RjdC18gMxNQVvSiXvObLu29mOFmkgdsB4cRTlV+EE=
github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
@@ -1473,6 +1472,8 @@ github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59P
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700 h1:eNUVfm/RFLIi1G7flU5/ZRTHvd4kcVuzfRnL6OFlzCI=
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
+github.com/openkruise/kruise-api v0.7.0 h1:BBQotEfeZ2l1+R0uvlsVK2FN8C4RTlG+JT86ba2hOR4=
+github.com/openkruise/kruise-api v0.7.0/go.mod h1:nCf5vVOjQJX5OaV7Qi0Z51/Rn9cd7s5kVrg8YLgFp1I=
github.com/openservicemesh/osm v0.3.0 h1:U88Nv1xm+7M+xYNkwjYVU6WSMp3MHIObTcO+gH20nOw=
github.com/openservicemesh/osm v0.3.0/go.mod h1:gyK0vN5ENnP26Y8huqgeTR52fOotZhnEorie215FnpU=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
@@ -2561,6 +2562,7 @@ k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0H
k8s.io/api v0.0.0-20190918195907-bd6ac527cfd2/go.mod h1:AOxZTnaXR/xiarlQL0JUfwQPxjmKDvVYoRp58cA7lUo=
k8s.io/api v0.0.0-20191115095533-47f6de673b26/go.mod h1:iA/8arsvelvo4IDqIhX4IbjTEKBGgvsf2OraTuRtLFU=
k8s.io/api v0.0.0-20191122220107-b5267f2975e0/go.mod h1:vYpRfxYkMrmPPSesoHEkGNHxNKTk96REAwqm/inQbs0=
+k8s.io/api v0.15.8/go.mod h1:hpDXsOhY/unVSSzhMol7kihWaNMf2snhF4nejjlzUzk=
k8s.io/api v0.16.4/go.mod h1:AtzMnsR45tccQss5q8RnF+W8L81DH6XwXwo/joEx9u0=
k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
@@ -2572,7 +2574,6 @@ k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8=
k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78=
k8s.io/api v0.18.3/go.mod h1:UOaMwERbqJMfeeeHc8XJKawj4P9TgDRnViIqqBeH2QA=
k8s.io/api v0.18.4/go.mod h1:lOIQAKYgai1+vz9J7YcDZwC26Z0zQewYOGWdyIPUUQ4=
-k8s.io/api v0.18.6 h1:osqrAXbOQjkKIWDTjrqxWQ3w0GkKb1KA1XkUGHHYpeE=
k8s.io/api v0.18.6/go.mod h1:eeyxr+cwCjMdLAmr2W3RyDI0VvTawSg/3RFFBEnmZGI=
k8s.io/api v0.18.7-rc.0/go.mod h1:v6x7KyKMJ7W/BbG7E9olOQshfszuXKKsxfnjaq+ylrk=
k8s.io/api v0.18.8 h1:aIKUzJPb96f3fKec2lxtY7acZC9gQNDLVhfSGpxBAC4=
@@ -2583,7 +2584,6 @@ k8s.io/apiextensions-apiserver v0.16.4/go.mod h1:HYQwjujEkXmQNhap2C9YDdIVOSskGZ3
k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs=
k8s.io/apiextensions-apiserver v0.17.6/go.mod h1:Z3CHLP3Tha+Rbav7JR3S+ye427UaJkHBomK2c4XtZ3A=
k8s.io/apiextensions-apiserver v0.18.0/go.mod h1:18Cwn1Xws4xnWQNC00FLq1E350b9lUF+aOdIWDOZxgo=
-k8s.io/apiextensions-apiserver v0.18.2 h1:I4v3/jAuQC+89L3Z7dDgAiN4EOjN6sbm6iBqQwHTah8=
k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY=
k8s.io/apiextensions-apiserver v0.18.4/go.mod h1:NYeyeYq4SIpFlPxSAB6jHPIdvu3hL0pc36wuRChybio=
k8s.io/apiextensions-apiserver v0.18.6 h1:vDlk7cyFsDyfwn2rNAO2DbmUbvXy5yT5GE3rrqOzaMo=
@@ -2597,6 +2597,7 @@ k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d/go.mod h1:3jediapYqJ2w1BF
k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4=
k8s.io/apimachinery v0.0.0-20191115015347-3c7067801da2/go.mod h1:dXFS2zaQR8fyzuvRdJDHw2Aerij/yVGJSre0bZQSVJA=
k8s.io/apimachinery v0.0.0-20191121175448-79c2a76c473a/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
+k8s.io/apimachinery v0.15.8/go.mod h1:Xc10RHc1U+F/e9GCloJ8QAeCGevSVP5xhOhqlE+e1kM=
k8s.io/apimachinery v0.16.4/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ=
k8s.io/apimachinery v0.16.5-beta.1/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ=
k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
@@ -2611,7 +2612,6 @@ k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftc
k8s.io/apimachinery v0.18.3/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
k8s.io/apimachinery v0.18.4/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
k8s.io/apimachinery v0.18.5/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
-k8s.io/apimachinery v0.18.6 h1:RtFHnfGNfd1N0LeSrKCUznz5xtUP1elRGvHJbL3Ntag=
k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
k8s.io/apimachinery v0.18.7-rc.0/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
k8s.io/apimachinery v0.18.8 h1:jimPrycCqgx2QPearX3to1JePz7wSbVLq+7PdBTTwQ0=
@@ -2625,7 +2625,6 @@ k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo=
k8s.io/apiserver v0.17.4/go.mod h1:5ZDQ6Xr5MNBxyi3iUZXS84QOhZl+W7Oq2us/29c0j9I=
k8s.io/apiserver v0.17.6/go.mod h1:sAYqm8hUDNA9aj/TzqwsJoExWrxprKv0tqs/z88qym0=
k8s.io/apiserver v0.18.0/go.mod h1:3S2O6FeBBd6XTo0njUrLxiqk8GNy6wWOftjhJcXYnjw=
-k8s.io/apiserver v0.18.2 h1:fwKxdTWwwYhxvtjo0UUfX+/fsitsNtfErPNegH2x9ic=
k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw=
k8s.io/apiserver v0.18.4/go.mod h1:q+zoFct5ABNnYkGIaGQ3bcbUNdmPyOCoEBcg51LChY8=
k8s.io/apiserver v0.18.6/go.mod h1:Zt2XvTHuaZjBz6EFYzpp+X4hTmgWGy8AthNVnTdm3Wg=
@@ -2641,6 +2640,7 @@ k8s.io/cloud-provider v0.17.4/go.mod h1:XEjKDzfD+b9MTLXQFlDGkk6Ho8SGMpaU8Uugx/KN
k8s.io/code-generator v0.0.0-20190612205613-18da4a14b22b/go.mod h1:G8bQwmHm2eafm5bgtX67XDZQ8CWKSGu9DekI+yN4Y5I=
k8s.io/code-generator v0.0.0-20190831074504-732c9ca86353/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE=
k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE=
+k8s.io/code-generator v0.15.8/go.mod h1:G8bQwmHm2eafm5bgtX67XDZQ8CWKSGu9DekI+yN4Y5I=
k8s.io/code-generator v0.16.4/go.mod h1:mJUgkl06XV4kstAnLHAIzJPVCOzVR+ZcfPIv4fUsFCY=
k8s.io/code-generator v0.17.1/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
@@ -2700,6 +2700,7 @@ k8s.io/kubectl v0.18.6/go.mod h1:3TLzFOrF9h4mlRPAvdNkDbs5NWspN4e0EnPnEB41CGo=
k8s.io/kubernetes v1.11.10/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8=
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
+k8s.io/kubernetes v1.14.7 h1:wJx/r2HuPVaaBeCUk/P47GSK0eyrj3mI/kESRFBp6/A=
k8s.io/kubernetes v1.14.7/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/legacy-cloud-providers v0.17.0/go.mod h1:DdzaepJ3RtRy+e5YhNtrCYwlgyK87j/5+Yfp0L9Syp8=
k8s.io/legacy-cloud-providers v0.17.4/go.mod h1:FikRNoD64ECjkxO36gkDgJeiQWwyZTuBkhu+yxOc1Js=
@@ -2787,10 +2788,9 @@ sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3L
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
sigs.k8s.io/structured-merge-diff v0.0.0-20190302045857-e85c7b244fd2/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
+sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
-sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ=
sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA=
-sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU=
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
sigs.k8s.io/structured-merge-diff v1.0.1 h1:LOs1LZWMsz1xs77Phr/pkB4LFaavH7IVq/3+WTN9XTA=
sigs.k8s.io/structured-merge-diff v1.0.1/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA=
diff --git a/hack/artifacthub/artifacthub-repo.yml b/hack/artifacthub/artifacthub-repo.yml
new file mode 100644
index 000000000..1cffa6cbd
--- /dev/null
+++ b/hack/artifacthub/artifacthub-repo.yml
@@ -0,0 +1,23 @@
+# Artifact Hub repository metadata file
+#
+# Some settings like the verified publisher flag or the ignored packages won't
+# be applied until the next time the repository is processed. Please keep in
+# mind that the repository won't be processed if it has not changed since the
+# last time it was processed. Depending on the repository kind, this is checked
+# in a different way. For Helm http based repositories, we consider it has
+# changed if the `index.yaml` file changes. For git based repositories, it does
+# when the hash of the last commit in the branch you set up changes. This does
+# NOT apply to ownership claim operations, which are processed immediately.
+#
+repositoryID: ARTIFACT_HUB_REPOSITORY_ID
+owners:
+ - name: Lei Zhang (Harry)
+ email: resouer@gmail.com
+ - name: Jianbo Sun
+ email: wonderflow.sun@gmail.com
+ - name: Ryan Zhang
+ email: yangzhangrice@hotmail.com
+ - name: Hongchao Deng
+ email: hongchaodeng1@gmail.com
+ - name: Zheng Xi Zhou
+ email: zzxwill@gmail.com
diff --git a/hack/crd/update.go b/hack/crd/update.go
index a957525aa..1ce35d813 100644
--- a/hack/crd/update.go
+++ b/hack/crd/update.go
@@ -3,65 +3,61 @@ package main
import (
"fmt"
"io/ioutil"
- "log"
"os"
- "path/filepath"
"strings"
)
func main() {
- dir, err := os.Getwd()
- if err != nil {
- log.Fatal(err)
+ var crds []string
+ args := os.Args
+ if len(args) <= 1 {
+ fmt.Println("no CRDs is specified")
+ os.Exit(1)
}
- if len(os.Args) > 1 {
- dir = os.Args[1]
- }
- err = FixNewSchemaValidationCheck(dir)
- if err != nil {
- fmt.Fprintln(os.Stderr, "error getting chart source:", err)
+ crds = args[1:]
+ if err := fixNewSchemaValidationCheck(crds); err != nil {
+ fmt.Println(err)
os.Exit(1)
}
}
-// FixNewSchemaValidationCheck temporarily corrects spec.validation.openAPIV3Schema issue, and it would be removed
-// after this issue was fixed https://github.com/oam-dev/kubevela/issues/284.
-func FixNewSchemaValidationCheck(chartPath string) error {
- err := filepath.Walk(chartPath, func(path string, info os.FileInfo, err error) error {
+func fixNewSchemaValidationCheck(crds []string) error {
+ for _, crd := range crds {
+ data, err := ioutil.ReadFile(crd)
if err != nil {
- fmt.Fprintln(os.Stderr, "failed to list the content of", path)
+ fmt.Fprintf(os.Stderr, "reading CRD file %s hit an issue: %s\n", crd, err)
return err
}
- if info.IsDir() {
- return nil
- }
+ var newData []string
+ // temporarily corrects spec.validation.openAPIV3Schema issue https://github.com/kubernetes/kubernetes/issues/91395
+ if strings.HasSuffix(crd, "charts/vela-core/crds/standard.oam.dev_podspecworkloads.yaml") {
+ var previousLine string
+ for _, line := range strings.Split(string(data), "\n") {
+ if strings.Contains(previousLine, "protocol:") &&
+ strings.Contains(line, "description: Protocol for port. Must be UDP, TCP,") {
+ tmp := strings.Split(line, "description")
- if info.Name() != "standard.oam.dev_podspecworkloads.yaml" {
- return nil
- }
- data, err := ioutil.ReadFile(path)
- if err != nil {
- fmt.Fprintln(os.Stderr, "open path err", path, err)
- return err
- }
- var newdata []string
- var previousLine string
- for _, line := range strings.Split(string(data), "\n") {
- if strings.Contains(previousLine, "protocol:") &&
- strings.Contains(line, "description: Protocol for port. Must be UDP, TCP,") {
- tmp := strings.Split(line, "description")
-
- if len(tmp) > 0 {
- blanks := tmp[0]
- defaultStr := blanks + "default: TCP"
- newdata = append(newdata, defaultStr)
+ if len(tmp) > 0 {
+ blanks := tmp[0]
+ defaultStr := blanks + "default: TCP"
+ newData = append(newData, defaultStr)
+ }
}
+ newData = append(newData, line)
+ previousLine = line
}
- newdata = append(newdata, line)
- previousLine = line
+ ioutil.WriteFile(crd, []byte(strings.Join(newData, "\n")), 0644)
}
-
- return ioutil.WriteFile(path, []byte(strings.Join(newdata, "\n")), info.Mode())
- })
- return err
+ // fix issue https://github.com/oam-dev/kubevela/issues/993
+ if strings.HasSuffix(crd, "legacy/charts/vela-core-legacy/crds/standard.oam.dev_routes.yaml") {
+ for _, line := range strings.Split(string(data), "\n") {
+ if strings.Contains(line, "default: Issuer") {
+ continue
+ }
+ newData = append(newData, line)
+ }
+ ioutil.WriteFile(crd, []byte(strings.Join(newData, "\n")), 0644)
+ }
+ }
+ return nil
}
diff --git a/hack/vela-templates/definitions/ingress.yaml b/hack/vela-templates/definitions/ingress.yaml
index 6a460e661..fd0bb9959 100644
--- a/hack/vela-templates/definitions/ingress.yaml
+++ b/hack/vela-templates/definitions/ingress.yaml
@@ -6,8 +6,17 @@ metadata:
Please use route trait in cap center for advanced usage."
name: ingress
spec:
+ status:
+ customStatus: |-
+ if len(context.outputs.ingress.status.loadBalancer.ingress) > 0 {
+ message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + context.outputs.ingress.status.loadBalancer.ingress[0].ip
+ }
+ if len(context.outputs.ingress.status.loadBalancer.ingress) == 0 {
+ message: "No loadBalancer found, visiting by using 'vela port-forward " + context.appName + " --route'\n"
+ }
+ healthPolicy: |
+ isHealth: len(context.outputs.service.spec.clusterIP) > 0
appliesToWorkloads:
- webservice
- worker
- extension:
- template: |
+ template: |
diff --git a/hack/vela-templates/definitions/manualscale.yaml b/hack/vela-templates/definitions/manualscale.yaml
index ce5b7cc38..246291d44 100644
--- a/hack/vela-templates/definitions/manualscale.yaml
+++ b/hack/vela-templates/definitions/manualscale.yaml
@@ -11,5 +11,4 @@ spec:
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
- extension:
- template: |-
+ template: |
diff --git a/hack/vela-templates/definitions/task.yaml b/hack/vela-templates/definitions/task.yaml
index 0c45ef657..525794a31 100644
--- a/hack/vela-templates/definitions/task.yaml
+++ b/hack/vela-templates/definitions/task.yaml
@@ -7,5 +7,4 @@ metadata:
spec:
definitionRef:
name: jobs.batch
- extension:
- template: |
+ template: |
diff --git a/hack/vela-templates/definitions/webservice.yaml b/hack/vela-templates/definitions/webservice.yaml
index 6bc007ebc..ca03df0dc 100644
--- a/hack/vela-templates/definitions/webservice.yaml
+++ b/hack/vela-templates/definitions/webservice.yaml
@@ -8,5 +8,4 @@ metadata:
spec:
definitionRef:
name: deployments.apps
- extension:
- template: |
+ template: |
diff --git a/hack/vela-templates/definitions/worker.yaml b/hack/vela-templates/definitions/worker.yaml
index 18e818b79..9aea430c5 100644
--- a/hack/vela-templates/definitions/worker.yaml
+++ b/hack/vela-templates/definitions/worker.yaml
@@ -7,5 +7,4 @@ metadata:
spec:
definitionRef:
name: deployments.apps
- extension:
- template: |
+ template: |
diff --git a/hack/vela-templates/gen_definitions.sh b/hack/vela-templates/gen_definitions.sh
index 2e5c55d23..1ee5ece1e 100755
--- a/hack/vela-templates/gen_definitions.sh
+++ b/hack/vela-templates/gen_definitions.sh
@@ -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%.*}"
diff --git a/legacy/README.md b/legacy/README.md
new file mode 100644
index 000000000..a80d4c882
--- /dev/null
+++ b/legacy/README.md
@@ -0,0 +1,38 @@
+# Legacy Support
+
+Now lots of apps are still running on Kubernetes clusters version v1.14 or v1.15, while KubeVela core requires the minimum
+Kubernetes version to be v1.16+.
+
+Currently, the main blocker is KubeVela uses CRD v1, while those old Kubernetes versions don't support CRD v1.
+So we generate v1beta1 CRD here for convenience. But we have no guarantee that KubeVela core will support the
+legacy Kubernetes versions.
+
+Follow the instructions in [README](../README.md) to create a namespace like `vela-system` and add the OAM Kubernetes
+Runtime helm repo.
+
+```
+$ kubectl create namespace vela-system
+$ helm repo add kubevela https://kubevelacharts.oss-cn-hangzhou.aliyuncs.com/core
+```
+
+Run the following command to install a KubeVela core legacy chart.
+
+```
+$ helm install -n vela-system vela-core-legacy kubevela/vela-core-legacy
+```
+
+If you'd like to install an older version of the legacy chart, use `helm search` to choose a proper chart version.
+```
+$ helm search repo vela-core-legacy -l
+ NAME CHART VERSION APP VERSION DESCRIPTION
+ kubevela/vela-core-legacy 0.2 0.2 A Helm chart for legacy KubeVela core Controlle...
+ kubevela/vela-core-legacy 0.0.1 0.1 A Helm chart for legacy KubeVela core Controlle...
+
+$ helm install -n vela-system kubevela-legacy kubevela/vela-core-legacy --version 0.0.1
+```
+
+Install the legacy chart as below if you want a nightly version.
+
+```
+$ helm install -n vela-system vela-core-legacy kubevela/vela-core-legacy --set image.tag=master
+```
diff --git a/legacy/charts/vela-core-legacy/Chart.yaml b/legacy/charts/vela-core-legacy/Chart.yaml
new file mode 100644
index 000000000..a58bb8e9f
--- /dev/null
+++ b/legacy/charts/vela-core-legacy/Chart.yaml
@@ -0,0 +1,21 @@
+apiVersion: v1
+name: vela-core-legacy
+description: A Helm chart for legacy KubeVela Core Controller, targeted on Kubernetes v1.14 and v1.15
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application.
+appVersion: 0.1.0
diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationconfigurations.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationconfigurations.yaml
index 147b82953..5fe8abbde 100644
--- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationconfigurations.yaml
+++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationconfigurations.yaml
@@ -386,6 +386,10 @@ spec:
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
scopes:
description: Scopes associated with this workload.
items:
diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationdeployments.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationdeployments.yaml
index 631cd76b4..8e77ed672 100644
--- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationdeployments.yaml
+++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationdeployments.yaml
@@ -108,7 +108,7 @@ spec:
description: Paused the rollout, default is false
type: boolean
rolloutBatches:
- description: The exact distribution among batches. mutually exclusive to NumBatches
+ description: The exact distribution among batches. mutually exclusive to NumBatches. The total number cannot exceed the targetSize or the size of the source resource We will IGNORE the last batch's replica field if it's a percentage since round errors can lead to inaccurate sum We highly recommend to leave the last batch's replica field empty
items:
description: RolloutBatch is used to describe how the each batch rollout should be
properties:
@@ -210,7 +210,7 @@ spec:
anyOf:
- type: integer
- type: string
- description: 'Replicas is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods it is mutually exclusive with the PodList field'
+ description: 'Replicas is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods we will ignore the percentage of the last batch to just fill the gap it is mutually exclusive with the PodList field'
x-kubernetes-int-or-string: true
type: object
type: array
@@ -218,7 +218,7 @@ spec:
description: RolloutStrategy defines strategies for the rollout plan
type: string
rolloutWebhooks:
- description: RolloutWebhooks provides a way for the rollout to interact with an external process
+ description: RolloutWebhooks provide a way for the rollout to interact with an external process
items:
description: RolloutWebhook holds the reference to external checks used for canary analysis
properties:
@@ -258,11 +258,10 @@ spec:
type: string
required:
- rolloutPlan
- - sourceApplicationName
- targetApplicationName
type: object
status:
- description: RolloutStatus defines the observed state of Rollout
+ description: ApplicationDeploymentStatus defines the observed state of ApplicationDeployment
properties:
batchRollingState:
description: BatchRollingState only meaningful when the Status is rolling
@@ -296,17 +295,23 @@ spec:
type: object
type: array
currentBatch:
- description: The current batch the rollout is working on/blocked
+ description: The current batch the rollout is working on/blocked it starts from 0
format: int32
type: integer
+ lastAppliedPodTemplateIdentifier:
+ description: lastAppliedPodTemplateIdentifier is a string that uniquely represent the last pod template each workload type could use different ways to identify that so we cannot compare between resources We update this field only after a successful rollout
+ type: string
+ lastSourceApplicationName:
+ description: LastSourceApplicationName contains the name of the application that we need to upgrade from. We will restart the rollout if this is not the same as the spec
+ type: string
+ lastTargetApplicationName:
+ description: LastTargetApplicationName contains the name of the application that we upgraded to We will restart the rollout if this is not the same as the spec
+ type: string
rollingState:
description: RollingState is the Rollout State
type: string
- sourceGeneration:
- description: The source resource generation
- type: string
targetGeneration:
- description: The target resource generation
+ description: NewPodTemplateIdentifier is a string that uniquely represent the new pod template each workload type could use different ways to identify that so we cannot compare between resources
type: string
upgradedReadyReplicas:
description: UpgradedReplicas is the number of Pods upgraded by the rollout controller that have a Ready Condition.
@@ -318,9 +323,8 @@ spec:
type: integer
required:
- currentBatch
+ - lastTargetApplicationName
- rollingState
- - sourceGeneration
- - targetGeneration
- upgradedReadyReplicas
- upgradedReplicas
type: object
diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applications.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applications.yaml
index 02c9031a6..212456c3f 100644
--- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applications.yaml
+++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applications.yaml
@@ -127,6 +127,37 @@ spec:
- type
type: object
type: array
+ services:
+ description: Services record the status of the application services
+ items:
+ description: ApplicationComponentStatus record the health status of App component
+ properties:
+ healthy:
+ type: boolean
+ message:
+ type: string
+ name:
+ type: string
+ traits:
+ items:
+ description: ApplicationTraitStatus records the trait health status
+ properties:
+ healthy:
+ type: boolean
+ message:
+ type: string
+ type:
+ type: string
+ required:
+ - healthy
+ - type
+ type: object
+ type: array
+ required:
+ - healthy
+ - name
+ type: object
+ type: array
status:
description: ApplicationPhase is a label for the condition of a application at the current time
type: string
diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_traitdefinitions.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_traitdefinitions.yaml
index eb78f7d86..a3093d35b 100644
--- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_traitdefinitions.yaml
+++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_traitdefinitions.yaml
@@ -67,6 +67,22 @@ spec:
revisionEnabled:
description: Revision indicates whether a trait is aware of component revision
type: boolean
+ status:
+ description: Status defines the custom health policy and status message for trait
+ properties:
+ customStatus:
+ description: CustomStatus defines the custom status message that could display to user
+ type: string
+ healthPolicy:
+ 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
diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_workloaddefinitions.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_workloaddefinitions.yaml
index 4a2e46a6f..28ecc1a4e 100644
--- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_workloaddefinitions.yaml
+++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_workloaddefinitions.yaml
@@ -81,6 +81,22 @@ 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
+ status:
+ description: Status defines the custom health policy and status message for workload
+ properties:
+ customStatus:
+ description: CustomStatus defines the custom status message that could display to user
+ type: string
+ healthPolicy:
+ 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
required:
- definitionRef
type: object
diff --git a/legacy/charts/vela-core-legacy/crds/standard.oam.dev_rollouts.yaml b/legacy/charts/vela-core-legacy/crds/standard.oam.dev_rollouts.yaml
deleted file mode 100644
index 9ef768ad7..000000000
--- a/legacy/charts/vela-core-legacy/crds/standard.oam.dev_rollouts.yaml
+++ /dev/null
@@ -1,350 +0,0 @@
-
----
-apiVersion: apiextensions.k8s.io/v1beta1
-kind: CustomResourceDefinition
-metadata:
- annotations:
- controller-gen.kubebuilder.io/version: v0.2.4
- creationTimestamp: null
- name: rollouts.standard.oam.dev
-spec:
- group: standard.oam.dev
- names:
- kind: Rollout
- listKind: RolloutList
- plural: rollouts
- singular: rollout
- scope: Namespaced
- subresources:
- status: {}
- validation:
- openAPIV3Schema:
- description: Rollout is the Schema for the rollouts 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: RolloutSpec defines the desired state of Rollout
- properties:
- rolloutPlan:
- description: RolloutPlan is the details on how to rollout the resources
- properties:
- canaryMetric:
- description: CanaryMetric provides a way for the rollout process to automatically check certain metrics before complete the process
- items:
- description: CanaryMetric holds the reference to metrics used for canary analysis
- properties:
- interval:
- description: Interval represents the windows size
- type: string
- metricsRange:
- description: Range value accepted for this metric
- properties:
- max:
- anyOf:
- - type: integer
- - type: string
- description: Maximum value
- x-kubernetes-int-or-string: true
- min:
- anyOf:
- - type: integer
- - type: string
- description: Minimum value
- x-kubernetes-int-or-string: true
- type: object
- name:
- description: Name of the metric
- type: string
- templateRef:
- description: TemplateRef references a metric template object
- 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:
- - name
- type: object
- type: array
- lastBatchToRollout:
- description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
- format: int32
- type: integer
- numBatches:
- description: The number of batches, default = 1 mutually exclusive to RolloutBatches
- format: int32
- type: integer
- rolloutBatches:
- description: The exact distribution among batches. mutually exclusive to NumBatches
- items:
- description: RolloutBatch is used to describe how the each batch rollout should be
- properties:
- batchRolloutWebhooks:
- description: RolloutWebhooks provides a way for the batch rollout to interact with an external process
- items:
- description: RolloutWebhook holds the reference to external checks used for canary analysis
- properties:
- metadata:
- additionalProperties:
- type: string
- description: Metadata (key-value pairs) for this webhook
- type: object
- name:
- description: Name of this webhook
- type: string
- timeout:
- description: Request timeout for this webhook
- type: string
- type:
- description: Type of this webhook
- type: string
- url:
- description: URL address of this webhook
- type: string
- required:
- - name
- - type
- - url
- type: object
- type: array
- canaryMetric:
- description: CanaryMetric provides a way for the batch rollout process to automatically check certain metrics before moving to the next batch
- items:
- description: CanaryMetric holds the reference to metrics used for canary analysis
- properties:
- interval:
- description: Interval represents the windows size
- type: string
- metricsRange:
- description: Range value accepted for this metric
- properties:
- max:
- anyOf:
- - type: integer
- - type: string
- description: Maximum value
- x-kubernetes-int-or-string: true
- min:
- anyOf:
- - type: integer
- - type: string
- description: Minimum value
- x-kubernetes-int-or-string: true
- type: object
- name:
- description: Name of the metric
- type: string
- templateRef:
- description: TemplateRef references a metric template object
- 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:
- - name
- type: object
- type: array
- instanceInterval:
- description: The wait time, in seconds, between instances upgrades, default = 0
- format: int32
- type: integer
- maxUnavailable:
- anyOf:
- - type: integer
- - type: string
- description: MaxUnavailable is the max allowed number of pods that is unavailable during the upgrade. We will mark the batch as ready as long as there are less or equal number of pods unavailable than this number. default = 0
- x-kubernetes-int-or-string: true
- podList:
- description: The list of Pods to get upgraded it is mutually exclusive with the Replica field
- items:
- type: string
- type: array
- replica:
- anyOf:
- - type: integer
- - type: string
- description: 'Replica is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods it is mutually exclusive with the PodList field'
- x-kubernetes-int-or-string: true
- type: object
- type: array
- rolloutWebhooks:
- description: RolloutWebhooks provides a way for the rollout to interact with an external process
- items:
- description: RolloutWebhook holds the reference to external checks used for canary analysis
- properties:
- metadata:
- additionalProperties:
- type: string
- description: Metadata (key-value pairs) for this webhook
- type: object
- name:
- description: Name of this webhook
- type: string
- timeout:
- description: Request timeout for this webhook
- type: string
- type:
- description: Type of this webhook
- type: string
- url:
- description: URL address of this webhook
- type: string
- required:
- - name
- - type
- - url
- type: object
- type: array
- stopped:
- description: Stopped the rollout, default is false
- type: boolean
- targetSize:
- description: The size of the target resource. The default is the same as the size of the source resource.
- format: int32
- type: integer
- type: object
- sourceRef:
- description: SourceRef references the source resource that contains the older version of the software. We assume that it's the first time to deploy when we cannot find the source.
- 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
- targetRef:
- description: TargetRef references a target resource that contains the newer version of the software. We assumed that new resource already exists. This is the only resource we work on if the resource is a stateful resource (cloneset/statefulset)
- 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:
- - rolloutPlan
- - targetRef
- type: object
- status:
- description: RolloutStatus defines the observed state of Rollout
- properties:
- batchRollingState:
- description: BatchRollingState only meaningful when the Status is rolling
- type: string
- conditions:
- description: Conditions represents the latest available observations of a CloneSet's current state.
- items:
- description: RolloutCondition is the condition of the rollout
- properties:
- batchRollingState:
- description: BatchRollingState only meaningful when the Status is rolling
- type: string
- lastTransitionTime:
- description: Last time the condition transitioned to this state
- format: date-time
- type: string
- message:
- description: A human readable message indicating details about the transition.
- type: string
- reason:
- description: The reason for the condition's last transition.
- type: string
- rollingState:
- description: RollingState is the Rollout Status
- type: string
- required:
- - rollingState
- type: object
- type: array
- currentBatch:
- description: The current batch the rollout is working on/blocked
- format: int32
- type: integer
- rollingState:
- description: RollingState is the Rollout State
- type: string
- sourceGeneration:
- description: The source resource generation
- type: string
- targetGeneration:
- description: The target resource generation
- type: string
- upgradedReplicas:
- description: UpgradedReplicas is the number of Pods upgraded by the rollout controller that have a Ready Condition.
- format: int32
- type: integer
- required:
- - currentBatch
- - rollingState
- - sourceGeneration
- - targetGeneration
- - upgradedReplicas
- type: object
- type: object
- version: v1alpha1
- versions:
- - name: v1alpha1
- served: true
- storage: true
-status:
- acceptedNames:
- kind: ""
- plural: ""
- conditions: []
- storedVersions: []
diff --git a/legacy/charts/vela-core-legacy/crds/standard.oam.dev_rollouttraits.yaml b/legacy/charts/vela-core-legacy/crds/standard.oam.dev_rollouttraits.yaml
index 9b5cfec33..64148c813 100644
--- a/legacy/charts/vela-core-legacy/crds/standard.oam.dev_rollouttraits.yaml
+++ b/legacy/charts/vela-core-legacy/crds/standard.oam.dev_rollouttraits.yaml
@@ -98,7 +98,7 @@ spec:
description: Paused the rollout, default is false
type: boolean
rolloutBatches:
- description: The exact distribution among batches. mutually exclusive to NumBatches
+ description: The exact distribution among batches. mutually exclusive to NumBatches. The total number cannot exceed the targetSize or the size of the source resource We will IGNORE the last batch's replica field if it's a percentage since round errors can lead to inaccurate sum We highly recommend to leave the last batch's replica field empty
items:
description: RolloutBatch is used to describe how the each batch rollout should be
properties:
@@ -200,7 +200,7 @@ spec:
anyOf:
- type: integer
- type: string
- description: 'Replicas is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods it is mutually exclusive with the PodList field'
+ description: 'Replicas is the number of pods to upgrade in this batch it can be an absolute number (ex: 5) or a percentage of total pods we will ignore the percentage of the last batch to just fill the gap it is mutually exclusive with the PodList field'
x-kubernetes-int-or-string: true
type: object
type: array
@@ -208,7 +208,7 @@ spec:
description: RolloutStrategy defines strategies for the rollout plan
type: string
rolloutWebhooks:
- description: RolloutWebhooks provides a way for the rollout to interact with an external process
+ description: RolloutWebhooks provide a way for the rollout to interact with an external process
items:
description: RolloutWebhook holds the reference to external checks used for canary analysis
properties:
@@ -288,7 +288,7 @@ spec:
- targetRef
type: object
status:
- description: RolloutStatus defines the observed state of Rollout
+ description: RolloutStatus defines the observed state of a rollout plan
properties:
batchRollingState:
description: BatchRollingState only meaningful when the Status is rolling
@@ -322,17 +322,17 @@ spec:
type: object
type: array
currentBatch:
- description: The current batch the rollout is working on/blocked
+ description: The current batch the rollout is working on/blocked it starts from 0
format: int32
type: integer
+ lastAppliedPodTemplateIdentifier:
+ description: lastAppliedPodTemplateIdentifier is a string that uniquely represent the last pod template each workload type could use different ways to identify that so we cannot compare between resources We update this field only after a successful rollout
+ type: string
rollingState:
description: RollingState is the Rollout State
type: string
- sourceGeneration:
- description: The source resource generation
- type: string
targetGeneration:
- description: The target resource generation
+ description: NewPodTemplateIdentifier is a string that uniquely represent the new pod template each workload type could use different ways to identify that so we cannot compare between resources
type: string
upgradedReadyReplicas:
description: UpgradedReplicas is the number of Pods upgraded by the rollout controller that have a Ready Condition.
@@ -345,8 +345,6 @@ spec:
required:
- currentBatch
- rollingState
- - sourceGeneration
- - targetGeneration
- upgradedReadyReplicas
- upgradedReplicas
type: object
diff --git a/legacy/charts/vela-core-legacy/crds/standard.oam.dev_routes.yaml b/legacy/charts/vela-core-legacy/crds/standard.oam.dev_routes.yaml
index b95fd166c..5faf081bb 100644
--- a/legacy/charts/vela-core-legacy/crds/standard.oam.dev_routes.yaml
+++ b/legacy/charts/vela-core-legacy/crds/standard.oam.dev_routes.yaml
@@ -37,6 +37,9 @@ spec:
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
@@ -113,7 +116,6 @@ spec:
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
diff --git a/pkg/appfile/addon_test.go b/pkg/appfile/addon_test.go
index c9c3e5dc0..6abdc6e72 100644
--- a/pkg/appfile/addon_test.go
+++ b/pkg/appfile/addon_test.go
@@ -27,7 +27,7 @@ var _ = It("Test ApplyTerraform", func() {
ioStream := util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
dm, _ := discoverymapper.New(cfg)
_, err := ApplyTerraform(app, k8sClient, ioStream, addonNamespace, dm)
- Expect(err.Error()).Should(Equal("exit status 1"))
+ Expect(err).ShouldNot(BeNil())
})
var _ = Describe("Test generateSecretFromTerraformOutput", func() {
diff --git a/pkg/appfile/api/appfile.go b/pkg/appfile/api/appfile.go
index 5c2a277d2..bb4248ff3 100644
--- a/pkg/appfile/api/appfile.go
+++ b/pkg/appfile/api/appfile.go
@@ -34,6 +34,11 @@ const (
DefaultUnknowFormatAppfilePath = "./Appfile"
)
+const (
+ // DefaultHealthScopeKey is the key in application for default health scope
+ DefaultHealthScopeKey = "healthscopes.core.oam.dev"
+)
+
// AppFile defines the spec of KubeVela Appfile
type AppFile struct {
Name string `json:"name"`
@@ -179,7 +184,7 @@ func addDefaultHealthScopeToApplication(app *v1alpha2.Application) *v1alpha2.Hea
health.Spec.WorkloadReferences = make([]v1alpha1.TypedReference, 0)
for i := range app.Spec.Components {
// FIXME(wonderflow): the hardcode health scope should be fixed.
- app.Spec.Components[i].Scopes = map[string]string{"healthscopes.core.oam.dev": health.Name}
+ app.Spec.Components[i].Scopes = map[string]string{DefaultHealthScopeKey: health.Name}
}
return health
}
diff --git a/pkg/appfile/parser.go b/pkg/appfile/parser.go
index a8c1fd8cd..c872dc97f 100644
--- a/pkg/appfile/parser.go
+++ b/pkg/appfile/parser.go
@@ -22,7 +22,7 @@ const (
// AppfileBuiltinConfig defines the built-in config variable
AppfileBuiltinConfig = "config"
- // OAMApplicationLabel is application's metadata label
+ // OAMApplicationLabel is application's metadata label tagged on AC and Component
OAMApplicationLabel = "application.oam.dev"
)
@@ -32,10 +32,12 @@ type Workload struct {
Type string
CapabilityCategory types.CapabilityCategory
Params map[string]interface{}
- Template string
- Health string
Traits []*Trait
Scopes []Scope
+
+ Template string
+ HealthCheckPolicy string
+ CustomStatusFormat string
}
// GetUserConfigName get user config from AppFile, it will contain config file in it.
@@ -56,12 +58,17 @@ func (wl *Workload) GetUserConfigName() string {
// EvalContext eval workload template and set result to context
func (wl *Workload) EvalContext(ctx process.Context) error {
- return definition.NewWDTemplater(wl.Name, wl.Template, "").Params(wl.Params).Complete(ctx)
+ return definition.NewWorkloadAbstractEngine(wl.Name).Params(wl.Params).Complete(ctx, wl.Template)
+}
+
+// EvalStatus eval workload status
+func (wl *Workload) EvalStatus(ctx process.Context, cli client.Client, ns string) (string, error) {
+ return definition.NewWorkloadAbstractEngine(wl.Name).Status(ctx, cli, ns, wl.CustomStatusFormat)
}
// EvalHealth eval workload health check
-func (wl *Workload) EvalHealth(ctx process.Context, client client.Client, name string) error {
- return definition.NewWDTemplater(wl.Name, "", wl.Health).Output(ctx, client, name).HealthCheck()
+func (wl *Workload) EvalHealth(ctx process.Context, client client.Client, namespace string) (bool, error) {
+ return definition.NewWorkloadAbstractEngine(wl.Name).HealthCheck(ctx, client, namespace, wl.HealthCheckPolicy)
}
// Scope defines the scope of workload
@@ -72,21 +79,29 @@ type Scope struct {
// Trait is ComponentTrait
type Trait struct {
+ // The Name is name of TraitDefinition, actually it's a type of the trait instance
Name string
CapabilityCategory types.CapabilityCategory
Params map[string]interface{}
+
Template string
- Health string
+ HealthCheckPolicy string
+ CustomStatusFormat string
}
// EvalContext eval trait template and set result to context
func (trait *Trait) EvalContext(ctx process.Context) error {
- return definition.NewTDTemplater(trait.Name, trait.Template, "").Params(trait.Params).Complete(ctx)
+ return definition.NewTraitAbstractEngine(trait.Name).Params(trait.Params).Complete(ctx, trait.Template)
+}
+
+// EvalStatus eval trait status
+func (trait *Trait) EvalStatus(ctx process.Context, cli client.Client, ns string) (string, error) {
+ return definition.NewTraitAbstractEngine(trait.Name).Status(ctx, cli, ns, trait.CustomStatusFormat)
}
// EvalHealth eval trait health check
-func (trait *Trait) EvalHealth(ctx process.Context, client client.Client, name string) error {
- return definition.NewTDTemplater(trait.Name, "", trait.Health).Output(ctx, client, name).HealthCheck()
+func (trait *Trait) EvalHealth(ctx process.Context, client client.Client, namespace string) (bool, error) {
+ return definition.NewTraitAbstractEngine(trait.Name).HealthCheck(ctx, client, namespace, trait.HealthCheckPolicy)
}
// Appfile describes application
@@ -142,7 +157,8 @@ func (p *Parser) parseWorkload(comp v1alpha2.ApplicationComponent) (*Workload, e
}
workload.CapabilityCategory = templ.CapabilityCategory
workload.Template = templ.TemplateStr
- workload.Health = templ.Health
+ workload.HealthCheckPolicy = templ.Health
+ workload.CustomStatusFormat = templ.CustomStatus
settings, err := util.RawExtension2Map(&comp.Settings)
if err != nil {
return nil, errors.WithMessagef(err, "fail to parse settings for %s", comp.Name)
@@ -187,7 +203,8 @@ func (p *Parser) parseTrait(name string, properties map[string]interface{}) (*Tr
CapabilityCategory: templ.CapabilityCategory,
Params: properties,
Template: templ.TemplateStr,
- Health: templ.Health,
+ HealthCheckPolicy: templ.Health,
+ CustomStatusFormat: templ.CustomStatus,
}, nil
}
@@ -213,10 +230,10 @@ func (p *Parser) GenerateApplicationConfiguration(app *Appfile, ns string) (*v1a
}
for _, tr := range wl.Traits {
if err := tr.EvalContext(pCtx); err != nil {
- return nil, nil, err
+ return nil, nil, errors.Wrapf(err, "evaluate template trait=%s app=%s", tr.Name, wl.Name)
}
}
- comp, acComp, err := evalWorkloadWithContext(pCtx, wl)
+ comp, acComp, err := evalWorkloadWithContext(pCtx, wl, app.Name, wl.Name)
if err != nil {
return nil, nil, err
}
@@ -245,20 +262,19 @@ func (p *Parser) GenerateApplicationConfiguration(app *Appfile, ns string) (*v1a
}
// evalWorkloadWithContext evaluate the workload's template to generate component and ACComponent
-func evalWorkloadWithContext(pCtx process.Context, wl *Workload) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
+func evalWorkloadWithContext(pCtx process.Context, wl *Workload, appName, compName string) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
base, assists := pCtx.Output()
componentWorkload, err := base.Unstructured()
if err != nil {
- return nil, nil, err
+ return nil, nil, errors.Wrapf(err, "evaluate base template component=%s app=%s", compName, appName)
}
- workloadType := wl.Type
- labels := componentWorkload.GetLabels()
- if labels == nil {
- labels = map[string]string{oam.WorkloadTypeLabel: workloadType}
- } else {
- labels[oam.WorkloadTypeLabel] = workloadType
+
+ labels := map[string]string{
+ oam.WorkloadTypeLabel: wl.Type,
+ oam.LabelAppName: appName,
+ oam.LabelAppComponent: compName,
}
- componentWorkload.SetLabels(labels)
+ util.AddLabels(componentWorkload, labels)
component := &v1alpha2.Component{}
component.Spec.Workload.Object = componentWorkload
@@ -268,9 +284,17 @@ func evalWorkloadWithContext(pCtx process.Context, wl *Workload) (*v1alpha2.Comp
for _, assist := range assists {
tr, err := assist.Ins.Unstructured()
if err != nil {
- return nil, nil, err
+ return nil, nil, errors.Wrapf(err, "evaluate trait=%s template for component=%s app=%s", assist.Name, compName, appName)
}
- tr.SetLabels(map[string]string{oam.TraitTypeLabel: assist.Type})
+ labels := map[string]string{
+ oam.TraitTypeLabel: assist.Type,
+ oam.LabelAppName: appName,
+ oam.LabelAppComponent: compName,
+ }
+ if assist.Name != "" {
+ labels[oam.TraitResource] = assist.Name
+ }
+ util.AddLabels(tr, labels)
acComponent.Traits = append(acComponent.Traits, v1alpha2.ComponentTrait{
Trait: runtime.RawExtension{
Object: tr,
@@ -282,7 +306,7 @@ func evalWorkloadWithContext(pCtx process.Context, wl *Workload) (*v1alpha2.Comp
// PrepareProcessContext prepares a DSL process Context
func PrepareProcessContext(k8sClient client.Client, wl *Workload, applicationName string, namespace string) (process.Context, error) {
- pCtx := process.NewContext(wl.Name)
+ pCtx := process.NewContext(wl.Name, applicationName)
userConfig := wl.GetUserConfigName()
if userConfig != "" {
cg := config.Configmap{Client: k8sClient}
@@ -290,12 +314,12 @@ func PrepareProcessContext(k8sClient client.Client, wl *Workload, applicationNam
var envName = namespace
data, err := cg.GetConfigData(config.GenConfigMapName(applicationName, wl.Name, userConfig), envName)
if err != nil {
- return nil, err
+ return nil, errors.Wrapf(err, "get config=%s for app=%s in namespace=%s", userConfig, applicationName, namespace)
}
pCtx.SetConfigs(data)
}
if err := wl.EvalContext(pCtx); err != nil {
- return nil, err
+ return nil, errors.Wrapf(err, "evaluate base template app=%s in namespace=%s", applicationName, namespace)
}
return pCtx, nil
}
diff --git a/pkg/appfile/parser_test.go b/pkg/appfile/parser_test.go
index 95f70b70d..851d3a6fb 100644
--- a/pkg/appfile/parser_test.go
+++ b/pkg/appfile/parser_test.go
@@ -401,7 +401,9 @@ var _ = Describe("Test appFile parser", func() {
"kind": "ManualScalerTrait",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
- "trait.oam.dev/type": "scaler",
+ "app.oam.dev/component": "myweb",
+ "app.oam.dev/name": "test",
+ "trait.oam.dev/type": "scaler",
},
},
"spec": map[string]interface{}{"replicaCount": int64(10)},
@@ -413,7 +415,8 @@ var _ = Describe("Test appFile parser", func() {
},
},
}
- Expect(ac).To(BeEquivalentTo(expectAppConfig))
+ fmt.Println(cmp.Diff(expectAppConfig, ac))
+ Expect(assert.ObjectsAreEqual(expectAppConfig, ac)).To(Equal(true))
expectComponent := &v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
@@ -432,6 +435,8 @@ var _ = Describe("Test appFile parser", func() {
"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{}{
@@ -464,8 +469,7 @@ var _ = Describe("Test appFile parser", func() {
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(fmt.Sprintf("diff %+v", cmp.Diff(components[0].Spec.Workload.Object,
- expectComponent.Spec.Workload.Object)))
+ 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())
})
diff --git a/pkg/builtin/build/build.go b/pkg/builtin/build/build.go
index f6f7b308a..27d4d209f 100644
--- a/pkg/builtin/build/build.go
+++ b/pkg/builtin/build/build.go
@@ -106,7 +106,7 @@ func (b *Build) buildImage(io cmdutil.IOStreams, image string) error {
io.Errorf("BuildImage wait for command execution error:%s", err.Error())
return err
}
- return b.pushImage(io, image)
+ return nil
}
func (b *Build) pushImage(io cmdutil.IOStreams, image string) error {
diff --git a/pkg/commands/capability.go b/pkg/commands/capability.go
index 50fb21b46..783e9e265 100644
--- a/pkg/commands/capability.go
+++ b/pkg/commands/capability.go
@@ -226,6 +226,7 @@ func NewCapCenterRemoveCommand(ioStreams cmdutil.IOStreams) *cobra.Command {
func listCapCenters(ioStreams cmdutil.IOStreams) error {
table := newUITable()
+ table.MaxColWidth = 80
table.AddRow("NAME", "ADDRESS")
capabilityCenterList, err := serverlib.ListCapabilityCenters()
if err != nil {
diff --git a/pkg/commands/cli.go b/pkg/commands/cli.go
index 657ddde2c..4301ff347 100644
--- a/pkg/commands/cli.go
+++ b/pkg/commands/cli.go
@@ -26,7 +26,7 @@ func NewCommand() *cobra.Command {
DisableFlagParsing: true,
Run: func(cmd *cobra.Command, args []string) {
allCommands := cmd.Commands()
- cmd.Printf("✈️ An Easy-to-use yet Fully Extensible App Platform based on Kubernetes and Open Application Model.\n\nUsage:\n vela [flags]\n vela [command]\n\nAvailable Commands:\n\n")
+ cmd.Printf("A Highly Extensible Platform Engine based on Kubernetes and Open Application Model.\n\nUsage:\n vela [flags]\n vela [command]\n\nAvailable Commands:\n\n")
PrintHelpByTag(cmd, allCommands, types.TypeStart)
PrintHelpByTag(cmd, allCommands, types.TypeApp)
PrintHelpByTag(cmd, allCommands, types.TypeCap)
@@ -58,11 +58,11 @@ func NewCommand() *cobra.Command {
NewInitCommand(commandArgs, ioStream),
NewUpCommand(commandArgs, ioStream),
NewExportCommand(commandArgs, ioStream),
+ NewCapabilityShowCommand(commandArgs, ioStream),
// Apps
NewListCommand(commandArgs, ioStream),
NewDeleteCommand(commandArgs, ioStream),
- NewAppShowCommand(commandArgs, ioStream),
NewAppStatusCommand(commandArgs, ioStream),
NewExecCommand(commandArgs, ioStream),
NewPortForwardCommand(commandArgs, ioStream),
diff --git a/pkg/commands/dashboard.go b/pkg/commands/dashboard.go
index 253dd8161..2d69f9a64 100644
--- a/pkg/commands/dashboard.go
+++ b/pkg/commands/dashboard.go
@@ -221,11 +221,13 @@ func OpenBrowser(url string) error {
func CheckVelaRuntimeInstalledAndReady(ioStreams cmdutil.IOStreams, c client.Client) (bool, error) {
if !helm.IsHelmReleaseRunning(types.DefaultKubeVelaReleaseName, types.DefaultKubeVelaChartName, types.DefaultKubeVelaNS, ioStreams) {
ioStreams.Info(fmt.Sprintf("\n%s %s", emojiFail, "KubeVela runtime is not installed yet."))
- ioStreams.Info(fmt.Sprintf("\n%s %s%s or %s",
+ ioStreams.Info(fmt.Sprintf("\n%s %s%s",
emojiLightBulb,
"Please use this command to install: ",
- white.Sprint("vela install -w"),
- white.Sprint("vela install --help")))
+ white.Sprint("helm repo add kubevela https://kubevelacharts.oss-cn-hangzhou.aliyuncs.com/core && "+
+ "helm repo update \n kubectl create namespace vela-system \n "+
+ "helm install -n vela-system kubevela kubevela/vela-core"),
+ ))
return false, nil
}
return PrintTrackVelaRuntimeStatus(context.Background(), c, ioStreams, 5*time.Minute)
diff --git a/pkg/commands/init.go b/pkg/commands/init.go
index b0dff468d..f9fd5f524 100644
--- a/pkg/commands/init.go
+++ b/pkg/commands/init.go
@@ -103,8 +103,7 @@ func NewInitCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
if deployStatus != compStatusDeployed {
return nil
}
-
- return printComponentStatus(context.Background(), o.client, o.IOStreams, o.workloadName, o.appName, o.Env)
+ return printAppStatus(context.Background(), newClient, ioStreams, o.appName, o.Env, cmd)
},
Annotations: map[string]string{
types.TagCommandType: types.TypeStart,
diff --git a/pkg/commands/refresh.go b/pkg/commands/refresh.go
index f63254e17..799217b95 100644
--- a/pkg/commands/refresh.go
+++ b/pkg/commands/refresh.go
@@ -11,7 +11,7 @@ import (
"github.com/fatih/color"
"github.com/gosuri/uitable"
- hashstructure "github.com/mitchellh/hashstructure/v2"
+ "github.com/mitchellh/hashstructure/v2"
"github.com/oam-dev/kubevela/apis/types"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
diff --git a/pkg/commands/show.go b/pkg/commands/show.go
index de2437cc8..6d8423c2c 100644
--- a/pkg/commands/show.go
+++ b/pkg/commands/show.go
@@ -40,8 +40,8 @@ const (
var webSite bool
-// NewAppShowCommand shows the reference doc for a workload type or trait
-func NewAppShowCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
+// NewCapabilityShowCommand shows the reference doc for a workload type or trait
+func NewCapabilityShowCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "show",
Short: "Show the reference doc for a workload type or trait",
diff --git a/pkg/commands/status.go b/pkg/commands/status.go
index a46e26302..3adede6df 100644
--- a/pkg/commands/status.go
+++ b/pkg/commands/status.go
@@ -4,11 +4,9 @@ import (
"context"
"fmt"
"os"
- "reflect"
"strings"
"time"
- runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@@ -19,8 +17,7 @@ import (
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/appfile/api"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
- "github.com/oam-dev/kubevela/pkg/oam"
- oam2 "github.com/oam-dev/kubevela/pkg/serverlib"
+ "github.com/oam-dev/kubevela/pkg/oam/util"
)
// HealthStatus represents health status strings.
@@ -46,10 +43,6 @@ type WorkloadHealthCondition = v1alpha2.WorkloadHealthCondition
// ScopeHealthCondition holds health condition of a scope
type ScopeHealthCondition = v1alpha2.ScopeHealthCondition
-var (
- kindHealthScope = reflect.TypeOf(v1alpha2.HealthScope{}).Name()
-)
-
// CompStatus represents the status of a component during "vela init"
type CompStatus int
@@ -119,12 +112,7 @@ func printAppStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOSt
if err != nil {
return err
}
- namespace := env.Name
-
- targetServices, err := oam2.GetServicesWhenDescribingApplication(cmd, app)
- if err != nil {
- return err
- }
+ namespace := env.Namespace
cmd.Printf("About:\n\n")
table := newUITable()
@@ -135,97 +123,74 @@ func printAppStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOSt
cmd.Printf("%s\n\n", table.String())
cmd.Printf("Services:\n\n")
+ return loopCheckStatus(ctx, c, ioStreams, appName, env)
+}
- for _, svcName := range targetServices {
- if err := printComponentStatus(ctx, c, ioStreams, svcName, appName, env); err != nil {
+func loadRemoteApplication(c client.Client, ns string, name string) (*v1alpha2.Application, error) {
+ app := new(v1alpha2.Application)
+ err := c.Get(context.Background(), client.ObjectKey{
+ Namespace: ns,
+ Name: name,
+ }, app)
+ return app, err
+}
+
+func loopCheckStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOStreams, appName string, env *types.EnvMeta) error {
+ remoteApp, err := loadRemoteApplication(c, env.Namespace, appName)
+ if err != nil {
+ return err
+ }
+ for _, comp := range remoteApp.Spec.Components {
+ compName := comp.Name
+
+ healthStatus, healthInfo, err := healthCheckLoop(ctx, c, compName, appName, env)
+ if err != nil {
+ ioStreams.Info(healthInfo)
return err
}
- }
+ ioStreams.Infof(white.Sprintf(" - Name: %s\n", compName))
+ ioStreams.Infof(" Type: %s\n", comp.WorkloadType)
- return nil
-}
+ healthColor := getHealthStatusColor(healthStatus)
+ healthInfo = strings.ReplaceAll(healthInfo, "\n", "\n\t") // format healthInfo output
+ ioStreams.Infof(" %s %s\n", healthColor.Sprint(healthStatus), healthColor.Sprint(healthInfo))
-func printComponentStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOStreams, compName, appName string, env *types.EnvMeta) error {
- app, appConfig, err := getAppConfig(ctx, c, compName, appName, env)
- if err != nil {
- return err
- }
- if app == nil || appConfig == nil {
- return errors.New(ErrNotLoadAppConfig)
- }
- svc, ok := app.Services[compName]
- if !ok {
- return fmt.Errorf(ErrServiceNotFound, compName)
- }
- workloadType := svc.GetType()
-
- healthStatus, healthInfo, err := healthCheckLoop(ctx, c, compName, appName, env)
- if err != nil {
- ioStreams.Info(healthInfo)
- return err
- }
- ioStreams.Infof(white.Sprintf(" - Name: %s\n", compName))
- ioStreams.Infof(" Type: %s\n", workloadType)
-
- healthColor := getHealthStatusColor(healthStatus)
- healthInfo = strings.ReplaceAll(healthInfo, "\n", "\n\t") // format healthInfo output
- ioStreams.Infof(" %s %s\n", healthColor.Sprint(healthStatus), healthColor.Sprint(healthInfo))
-
- // workload Must found
- ioStreams.Infof(" Traits:\n")
- workloadStatus, _ := getWorkloadStatusFromAppConfig(appConfig, compName)
- for _, tr := range workloadStatus.Traits {
- traitType, traitInfo, err := traitCheckLoop(ctx, c, tr.Reference, compName, appConfig, app, 60*time.Second)
+ // load it again after health check
+ remoteApp, err = loadRemoteApplication(c, env.Namespace, appName)
if err != nil {
- ioStreams.Infof(" - %s%s: %s, err: %v", emojiFail, white.Sprint(traitType), traitInfo, err)
- continue
+ return err
}
- ioStreams.Infof(" - %s%s: %s", emojiSucceed, white.Sprint(traitType), traitInfo)
+ // workload Must found
+ ioStreams.Infof(" Traits:\n")
+ workloadStatus, _ := getWorkloadStatusFromApp(remoteApp, compName)
+ for _, tr := range workloadStatus.Traits {
+ if tr.Message != "" {
+ if tr.Healthy {
+ ioStreams.Infof(" - %s%s: %s", emojiSucceed, white.Sprint(tr.Type), tr.Message)
+ } else {
+ ioStreams.Infof(" - %s%s: %s", emojiFail, white.Sprint(tr.Type), tr.Message)
+ }
+ continue
+ }
+ var message string
+ for _, v := range comp.Traits {
+ if v.Name == tr.Type {
+ traitData, _ := util.RawExtension2Map(&v.Properties)
+ for k, v := range traitData {
+ message += fmt.Sprintf("%v=%v\n\t\t", k, v)
+ }
+ break
+ }
+ }
+ ioStreams.Infof(" - %s%s: %s", emojiSucceed, white.Sprint(tr.Type), message)
+ }
+ ioStreams.Info("")
+ ioStreams.Infof(" Last Deployment:\n")
+ ioStreams.Infof(" Created at: %v\n", remoteApp.CreationTimestamp)
}
- ioStreams.Info("")
- ioStreams.Infof(" Last Deployment:\n")
- ioStreams.Infof(" Created at: %v\n", appConfig.CreationTimestamp)
- ioStreams.Infof(" Updated at: %v\n", app.UpdateTime.Format(time.RFC3339))
return nil
}
-func traitCheckLoop(ctx context.Context, c client.Client, reference runtimev1alpha1.TypedReference, compName string, appConfig *v1alpha2.ApplicationConfiguration, app *api.Application, timeout time.Duration) (string, string, error) {
- tr, err := oam2.GetUnstructured(ctx, c, appConfig.Namespace, reference)
- if err != nil {
- return "", "", err
- }
- traitType, ok := tr.GetLabels()[oam.TraitTypeLabel]
- if !ok {
- message, err := oam2.GetStatusFromObject(tr)
- return traitType, message, err
- }
-
- checker := oam2.GetChecker(traitType, c)
-
- // Health Check Loop For Trait
- var message string
- sHealthCheck := newTrackingSpinner(fmt.Sprintf("Checking %s status ...", traitType))
- sHealthCheck.Start()
- defer sHealthCheck.Stop()
-CheckLoop:
- for {
- time.Sleep(trackingInterval)
- var check oam2.CheckStatus
- check, message, err = checker.Check(ctx, reference, compName, appConfig, app)
- if err != nil {
- message = red.Sprintf("%s check failed!", traitType)
- return traitType, message, err
- }
- if check == oam2.StatusDone {
- break CheckLoop
- }
- if time.Since(tr.GetCreationTimestamp().Time) >= timeout {
- return traitType, fmt.Sprintf("Checking timeout: %s", message), nil
- }
- }
- return traitType, message, nil
-}
-
func healthCheckLoop(ctx context.Context, c client.Client, compName, appName string, env *types.EnvMeta) (HealthStatus, string, error) {
// Health Check Loop For Workload
var healthInfo string
@@ -251,14 +216,6 @@ HealthCheckLoop:
return healthStatus, healthInfo, nil
}
-func tryGetWorkloadStatus(ctx context.Context, c client.Client, ns string, wlRef runtimev1alpha1.TypedReference) (string, error) {
- workload, err := oam2.GetUnstructured(ctx, c, ns, wlRef)
- if err != nil {
- return "", err
- }
- return oam2.GetStatusFromObject(workload)
-}
-
func printTrackingDeployStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOStreams, compName, appName string, env *types.EnvMeta) (CompStatus, error) {
sDeploy := newTrackingSpinnerWithDelay("Checking Status ...", trackingInterval)
sDeploy.Start()
@@ -314,30 +271,22 @@ func TrackDeployStatus(ctx context.Context, c client.Client, compName, appName s
return compStatusDeploying, "", nil
}
+// trackHealthCheckingStatus will check health status from health scope
func trackHealthCheckingStatus(ctx context.Context, c client.Client, compName, appName string, env *types.EnvMeta) (CompStatus, HealthStatus, string, error) {
- app, appConfig, err := getAppConfig(ctx, c, compName, appName, env)
+ app, err := loadRemoteApplication(c, env.Namespace, appName)
if err != nil {
return compStatusUnknown, HealthStatusNotDiagnosed, "", err
}
- if app == nil || appConfig == nil {
- return compStatusUnknown, HealthStatusNotDiagnosed, "", errors.New(ErrNotLoadAppConfig)
- }
- wlStatus, foundWlStatus := getWorkloadStatusFromAppConfig(appConfig, compName)
- // make sure component already initilized
- if !foundWlStatus {
- if len(appConfig.Status.Conditions) < 1 {
- // still reconciling
- return compStatusUnknown, HealthStatusUnknown, "", nil
- }
- appConfigConditionMsg := appConfig.Status.GetCondition(runtimev1alpha1.TypeSynced).Message
- return compStatusUnknown, HealthStatusUnknown, "", fmt.Errorf(ErrFmtNotInitialized, appConfigConditionMsg)
+ if len(app.Status.Conditions) < 1 {
+ // still reconciling
+ return compStatusUnknown, HealthStatusUnknown, "", nil
}
// check whether referenced a HealthScope
var healthScopeName string
- for _, v := range wlStatus.Scopes {
- if v.Reference.Kind == kindHealthScope {
- healthScopeName = v.Reference.Name
+ for _, v := range app.Spec.Components {
+ if len(v.Scopes) > 0 {
+ healthScopeName = v.Scopes[api.DefaultHealthScopeKey]
}
}
var healthStatus HealthStatus
@@ -360,38 +309,14 @@ func trackHealthCheckingStatus(ctx context.Context, c client.Client, compName, a
return compStatusHealthCheckDone, healthStatus, wlhc.Diagnosis, nil
}
if healthStatus == HealthStatusUnhealthy {
- cTime := appConfig.GetCreationTimestamp()
+ cTime := app.GetCreationTimestamp()
if time.Since(cTime.Time) <= healthCheckBufferTime {
return compStatusHealthChecking, HealthStatusUnknown, "", nil
}
return compStatusHealthCheckDone, healthStatus, wlhc.Diagnosis, nil
}
}
- // No health scope specified or health status is unknown , try get status from workload
- statusInfo, err := tryGetWorkloadStatus(ctx, c, env.Namespace, wlStatus.Reference)
- if err != nil {
- return compStatusUnknown, HealthStatusUnknown, "", err
- }
- return compStatusHealthCheckDone, HealthStatusNotDiagnosed, statusInfo, nil
-}
-
-func getAppConfig(ctx context.Context, c client.Client, compName, appName string, env *types.EnvMeta) (*api.Application, *v1alpha2.ApplicationConfiguration, error) {
- var app *api.Application
- var err error
- if appName != "" {
- app, err = appfile.LoadApplication(env.Name, appName)
- } else {
- app, err = appfile.MatchAppByComp(env.Name, compName)
- }
- if err != nil {
- return nil, nil, err
- }
-
- appConfig, err := appfile.GetAppConfig(ctx, c, app, env)
- if err != nil {
- return nil, nil, err
- }
- return app, appConfig, nil
+ return compStatusHealthCheckDone, HealthStatusNotDiagnosed, "", nil
}
func getApp(ctx context.Context, c client.Client, compName, appName string, env *types.EnvMeta) (*api.Application, *v1alpha2.Application, error) {
@@ -413,14 +338,14 @@ func getApp(ctx context.Context, c client.Client, compName, appName string, env
return app, appObj, nil
}
-func getWorkloadStatusFromAppConfig(appConfig *v1alpha2.ApplicationConfiguration, compName string) (v1alpha2.WorkloadStatus, bool) {
+func getWorkloadStatusFromApp(app *v1alpha2.Application, compName string) (v1alpha2.ApplicationComponentStatus, bool) {
foundWlStatus := false
- wlStatus := v1alpha2.WorkloadStatus{}
- if appConfig == nil {
+ wlStatus := v1alpha2.ApplicationComponentStatus{}
+ if app == nil {
return wlStatus, foundWlStatus
}
- for _, v := range appConfig.Status.Workloads {
- if v.ComponentName == compName {
+ for _, v := range app.Status.Services {
+ if v.Name == compName {
wlStatus = v
foundWlStatus = true
break
diff --git a/pkg/commands/system.go b/pkg/commands/system.go
index cdc22d143..262a29777 100644
--- a/pkg/commands/system.go
+++ b/pkg/commands/system.go
@@ -94,7 +94,7 @@ func (i *infoCmd) run(ioStreams cmdutil.IOStreams) error {
return fmt.Errorf("fail to get cluster chartPath: %w", err)
}
ioStreams.Info("Versions:")
- ioStreams.Infof("oam-kubernetes-runtime: %s \n", clusterVersion)
+ ioStreams.Infof("kubevela: %s \n", clusterVersion)
// TODO(wonderflow): we should print all helm charts installed by vela, including plugins
return nil
@@ -123,6 +123,7 @@ func NewInstallCommand(c types.Args, chartContent string, ioStreams cmdutil.IOSt
Annotations: map[string]string{
types.TagCommandType: types.TypeStart,
},
+ Deprecated: "vela install is DEPRECATED and we will remove it after Kubevela 1.0. Please use helm chart instead",
}
flag := cmd.Flags()
@@ -282,7 +283,7 @@ func GetOAMReleaseVersion(ns string) (string, error) {
return result.Chart.AppVersion(), nil
}
}
- return "", errors.New("oam-kubernetes-runtime not found in your kubernetes cluster, try `vela install` to install")
+ return "", errors.New("kubevela chart not found in your kubernetes cluster, refer to 'https://kubevela.io/#/en/install' for installation")
}
// PrintTrackVelaRuntimeStatus prints status of installing vela-core runtime
diff --git a/pkg/controller/common/rollout/rollout_plan_controller.go b/pkg/controller/common/rollout/rollout_plan_controller.go
new file mode 100644
index 000000000..3df2613c8
--- /dev/null
+++ b/pkg/controller/common/rollout/rollout_plan_controller.go
@@ -0,0 +1,255 @@
+package rollout
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/crossplane/crossplane-runtime/pkg/event"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/util/intstr"
+ "k8s.io/klog/v2"
+ "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/controller/common/rollout/workloads"
+ "github.com/oam-dev/kubevela/pkg/oam"
+)
+
+// the default time to check back if we still have work to do
+const rolloutReconcileRequeueTime = 5 * time.Second
+
+// Controller is the controller that controls the rollout plan resource
+type Controller struct {
+ client client.Client
+ recorder event.Recorder
+ parentController oam.Object
+
+ rolloutSpec *v1alpha1.RolloutPlan
+ rolloutStatus v1alpha1.RolloutStatus
+
+ targetWorkload *unstructured.Unstructured
+ sourceWorkload *unstructured.Unstructured
+}
+
+// NewRolloutPlanController creates a RolloutPlanController
+func NewRolloutPlanController(client client.Client, parentController oam.Object, recorder event.Recorder,
+ rolloutSpec *v1alpha1.RolloutPlan,
+ rolloutStatus v1alpha1.RolloutStatus, targetWorkload,
+ sourceWorkload *unstructured.Unstructured) *Controller {
+ return &Controller{
+ client: client,
+ parentController: parentController,
+ recorder: recorder,
+ rolloutSpec: rolloutSpec,
+ rolloutStatus: rolloutStatus,
+ targetWorkload: targetWorkload,
+ sourceWorkload: sourceWorkload,
+ }
+}
+
+// Reconcile reconciles a rollout plan
+func (r *Controller) Reconcile(ctx context.Context) (res reconcile.Result, status v1alpha1.RolloutStatus) {
+ klog.InfoS("Reconcile the rollout plan", "rollout Spec", r.rolloutSpec,
+ "target workload", klog.KObj(r.targetWorkload))
+ if r.sourceWorkload != nil {
+ klog.InfoS("we will do rolling upgrades", "source workload", klog.KObj(r.sourceWorkload))
+ }
+ klog.InfoS("rollout spec ", "rollout state", r.rolloutStatus.RollingState, "batch rolling state",
+ r.rolloutStatus.BatchRollingState, "current batch", r.rolloutStatus.CurrentBatch, "upgraded Replicas",
+ r.rolloutStatus.UpgradedReplicas)
+
+ defer klog.InfoS("Finished reconciling rollout plan", "rollout state", status.RollingState,
+ "batch rolling state", status.BatchRollingState, "current batch", status.CurrentBatch,
+ "upgraded Replicas", status.UpgradedReplicas, "reconcile result ", res)
+
+ status = r.rolloutStatus
+
+ defer func() {
+ if status.RollingState == v1alpha1.RolloutFailedState ||
+ status.RollingState == v1alpha1.RolloutSucceedState {
+ // no need to requeue if we reach the terminal states
+ res = reconcile.Result{}
+ } else {
+ res = reconcile.Result{
+ RequeueAfter: rolloutReconcileRequeueTime,
+ }
+ }
+ }()
+
+ workloadController, err := r.GetWorkloadController()
+ if err != nil {
+ r.rolloutStatus.RolloutFailed(err.Error())
+ r.recorder.Event(r.parentController, event.Warning("Unsupported workload", err))
+ return
+ }
+
+ switch r.rolloutStatus.RollingState {
+ case v1alpha1.VerifyingState:
+ status = *workloadController.Verify(ctx)
+
+ case v1alpha1.InitializingState:
+ // TODO: call the pre-rollout webhooks
+ status = *workloadController.Initialize(ctx)
+
+ case v1alpha1.RollingInBatchesState:
+ status = r.reconcileBatchInRolling(ctx, workloadController)
+
+ case v1alpha1.FinalisingState:
+ // TODO: call the post-rollout webhooks
+ status = *workloadController.Finalize(ctx)
+
+ case v1alpha1.RolloutSucceedState:
+ // Nothing to do
+
+ case v1alpha1.RolloutFailedState:
+ // Nothing to do
+
+ default:
+ panic(fmt.Sprintf("illegal rollout status %+v", r.rolloutStatus))
+ }
+
+ return res, status
+}
+
+// reconcile logic when we are in the middle of rollout
+func (r *Controller) reconcileBatchInRolling(ctx context.Context, workloadController workloads.WorkloadController) (
+ status v1alpha1.RolloutStatus) {
+
+ if r.rolloutSpec.Paused {
+ r.recorder.Event(r.parentController, event.Normal("Rollout paused", "Rollout paused"))
+ r.rolloutStatus.SetConditions(v1alpha1.NewPositiveCondition("Paused"))
+ return r.rolloutStatus
+ }
+
+ // makes sure that the current batch and replica count in the status are validate
+ replicas, err := workloadController.Size(ctx)
+ if err != nil {
+ r.rolloutStatus.RolloutRetry(err.Error())
+ return r.rolloutStatus
+ }
+ r.validateRollingBatchStatus(int(replicas))
+
+ switch r.rolloutStatus.BatchRollingState {
+ case v1alpha1.BatchInitializingState:
+ // TODO: call the pre-batch webhook
+
+ case v1alpha1.BatchInRollingState:
+ // still rolling the batch, the batch rolling is not completed yet
+ status = *workloadController.RolloutOneBatchPods(ctx)
+
+ case v1alpha1.BatchVerifyingState:
+ // verifying if the application is ready to roll
+ // need to check if they meet the availability requirements in the rollout spec.
+ // TODO: evaluate any metrics/analysis
+ status = *workloadController.CheckOneBatchPods(ctx)
+
+ case v1alpha1.BatchFinalizingState:
+ // all the pods in the are available
+ r.finalizeOneBatch()
+
+ case v1alpha1.BatchReadyState:
+ // all the pods in the are upgraded and their state are ready
+ // wait to move to the next batch if there are any
+ r.tryMovingToNextBatch()
+
+ default:
+ panic(fmt.Sprintf("illegal status %+v", r.rolloutStatus))
+ }
+
+ return status
+}
+
+// check if we can move to the next batch
+func (r *Controller) tryMovingToNextBatch() {
+ if r.rolloutSpec.BatchPartition == nil || *r.rolloutSpec.BatchPartition > r.rolloutStatus.CurrentBatch {
+ klog.InfoS("ready to rollout the next batch", "current batch", r.rolloutStatus.CurrentBatch)
+ r.rolloutStatus.CurrentBatch++
+ r.rolloutStatus.StateTransition(v1alpha1.BatchRolloutApprovedEvent)
+ } else {
+ klog.V(common.LogDebug).InfoS("the current batch is waiting to move on", "current batch",
+ r.rolloutStatus.CurrentBatch)
+ }
+}
+
+func (r *Controller) finalizeOneBatch() {
+ // TODO: call the post-batch webhooks if there are any
+ currentBatch := int(r.rolloutStatus.CurrentBatch)
+ if currentBatch == len(r.rolloutSpec.RolloutBatches)-1 {
+ // this is the last batch, mark the rollout finalized
+ r.rolloutStatus.StateTransition(v1alpha1.AllBatchFinishedEvent)
+ r.recorder.Event(r.parentController, event.Normal("all batches rolled out",
+ fmt.Sprintf("upgrade pod = %d, total ready pod = %d", r.rolloutStatus.UpgradedReplicas,
+ r.rolloutStatus.UpgradedReadyReplicas)))
+ } else {
+ klog.InfoS("finished one batch rollout", "current batch", r.rolloutStatus.CurrentBatch)
+ // th
+ r.recorder.Event(r.parentController, event.Normal("Batch finalized",
+ fmt.Sprintf("the batch num = %d is ready", r.rolloutStatus.CurrentBatch)))
+ r.rolloutStatus.StateTransition(v1alpha1.FinishedOneBatchEvent)
+ }
+}
+
+// verify that the upgradedReplicas and current batch in the status are valid according to the spec
+func (r *Controller) validateRollingBatchStatus(totalSize int) bool {
+ status := r.rolloutStatus
+ spec := r.rolloutSpec
+ podCount := 0
+ if spec.BatchPartition != nil && *spec.BatchPartition < status.CurrentBatch {
+ klog.ErrorS(fmt.Errorf("the current batch value in the status is greater than the batch partition"),
+ "batch partition", *spec.BatchPartition, "current batch status", status.CurrentBatch)
+ return false
+ }
+ upgradedReplicas := int(status.UpgradedReplicas)
+ currentBatch := int(status.CurrentBatch)
+ // calculate the lower bound of the possible pod count just before the current batch
+ for i, r := range spec.RolloutBatches {
+ if i < currentBatch {
+ batchSize, _ := intstr.GetValueFromIntOrPercent(&r.Replicas, totalSize, true)
+ podCount += batchSize
+ }
+ }
+ // the recorded number should be at least as much as the all the pods before the current batch
+ if podCount > upgradedReplicas {
+ klog.ErrorS(fmt.Errorf("the upgraded replica in the status is too small"), "upgraded num status",
+ upgradedReplicas, "pods in all the previous batches", podCount)
+ return false
+ }
+ // calculate the upper bound with the current batch
+ if currentBatch == len(spec.RolloutBatches)-1 {
+ // avoid round up problems
+ podCount = totalSize
+ } else {
+ batchSize, _ := intstr.GetValueFromIntOrPercent(&spec.RolloutBatches[currentBatch].Replicas,
+ totalSize, true)
+ podCount += batchSize
+ }
+ // the recorded number should be not as much as the all the pods including the active batch
+ if podCount < upgradedReplicas {
+ klog.ErrorS(fmt.Errorf("the upgraded replica in the status is too large"), "upgraded num status",
+ upgradedReplicas, "pods in the batches including the current batch", podCount)
+ return false
+ }
+ return true
+}
+
+// GetWorkloadController pick the right workload controller to work on the workload
+func (r *Controller) GetWorkloadController() (workloads.WorkloadController, error) {
+ kind := r.targetWorkload.GetObjectKind().GroupVersionKind().Kind
+ target := types.NamespacedName{
+ Namespace: r.targetWorkload.GetNamespace(),
+ Name: r.targetWorkload.GetName(),
+ }
+
+ switch kind {
+ case "CloneSet":
+ return workloads.NewCloneSetController(r.client, r.recorder, r.parentController,
+ r.rolloutSpec, &r.rolloutStatus, target), nil
+
+ default:
+ return nil, fmt.Errorf("the workload kind `%s` is not supported", kind)
+ }
+}
diff --git a/pkg/controller/common/rollout/rollout_plan_init.go b/pkg/controller/common/rollout/rollout_plan_init.go
deleted file mode 100644
index 321238ed8..000000000
--- a/pkg/controller/common/rollout/rollout_plan_init.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package rollout
-
-import (
- "context"
-
- "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/klog/v2"
- "sigs.k8s.io/controller-runtime/pkg/client"
-
- "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
-)
-
-// ReconcileRolloutPlan generates the rollout plan and reconcile it
-func ReconcileRolloutPlan(ctx context.Context, client client.Client, rolloutSpec *v1alpha1.RolloutPlan,
- targetWorkload, sourceWorkload *unstructured.Unstructured) error {
- klog.InfoS("generate the rollout plan", "rollout Spec", rolloutSpec,
- "target workload", klog.KObj(targetWorkload))
- return nil
-}
diff --git a/pkg/controller/standard.oam.dev/v1alpha1/rollout/webhook.go b/pkg/controller/common/rollout/rollout_webhook.go
similarity index 100%
rename from pkg/controller/standard.oam.dev/v1alpha1/rollout/webhook.go
rename to pkg/controller/common/rollout/rollout_webhook.go
diff --git a/pkg/controller/common/rollout/workloads/cloneset_controller.go b/pkg/controller/common/rollout/workloads/cloneset_controller.go
new file mode 100644
index 000000000..213870deb
--- /dev/null
+++ b/pkg/controller/common/rollout/workloads/cloneset_controller.go
@@ -0,0 +1,253 @@
+package workloads
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/crossplane/crossplane-runtime/pkg/event"
+ kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/util/intstr"
+ "k8s.io/klog/v2"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "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"
+)
+
+// CloneSetController is responsible for handle Cloneset type of workloads
+type CloneSetController struct {
+ client client.Client
+ recorder event.Recorder
+ parentController oam.Object
+
+ rolloutSpec *v1alpha1.RolloutPlan
+ rolloutStatus *v1alpha1.RolloutStatus
+ workloadNamespacedName types.NamespacedName
+ cloneSet *kruise.CloneSet
+}
+
+// NewCloneSetController creates a new Cloneset controller
+func NewCloneSetController(client client.Client, recorder event.Recorder, parentController oam.Object,
+ rolloutSpec *v1alpha1.RolloutPlan, rolloutStatus *v1alpha1.RolloutStatus, workloadName types.NamespacedName) *CloneSetController {
+ return &CloneSetController{
+ client: client,
+ recorder: recorder,
+ parentController: parentController,
+ rolloutSpec: rolloutSpec,
+ rolloutStatus: rolloutStatus,
+ workloadNamespacedName: workloadName,
+ }
+}
+
+// Size fetches the Cloneset and returns the replicas (not the actual number of pods)
+func (c *CloneSetController) Size(ctx context.Context) (int32, error) {
+ if c.cloneSet == nil {
+ err := c.fetchCloneSet(ctx)
+ if err != nil {
+ return 0, err
+ }
+ }
+ // default is 1
+ if c.cloneSet.Spec.Replicas == nil {
+ return 1, nil
+ }
+ return *c.cloneSet.Spec.Replicas, nil
+}
+
+// Verify verifies that the target rollout resource is consistent with the rollout spec
+func (c *CloneSetController) Verify(ctx context.Context) *v1alpha1.RolloutStatus {
+ var verifyErr error
+ defer func() {
+ if verifyErr != nil {
+ klog.Error(verifyErr)
+ c.recorder.Event(c.parentController, event.Warning("VerifyFailed", verifyErr))
+ }
+ }()
+
+ if verifyErr = c.fetchCloneSet(ctx); verifyErr != nil {
+ return c.rolloutStatus
+ }
+
+ // make sure that there are changes in the pod template
+ targetHash := c.cloneSet.Status.UpdateRevision
+ if targetHash == c.rolloutStatus.LastAppliedPodTemplateIdentifier {
+ verifyErr = fmt.Errorf("there is no difference between the source and target, hash = %s", targetHash)
+ c.rolloutStatus.RolloutFailed(verifyErr.Error())
+ return c.rolloutStatus
+ }
+ // record the new pod template hash
+ c.rolloutStatus.NewPodTemplateIdentifier = targetHash
+
+ // check if the rollout spec is compatible with the current state
+ totalReplicas, _ := c.Size(ctx)
+
+ // check if the target spec is the same as the Cloneset replicas
+ if verifyErr = c.verifyBatchSizes(totalReplicas); verifyErr != nil {
+ c.rolloutStatus.RolloutFailed(verifyErr.Error())
+ return c.rolloutStatus
+ }
+
+ // the rollout batch partition is either automatic or zero
+ if c.rolloutSpec.BatchPartition != nil && *c.rolloutSpec.BatchPartition != 0 {
+ verifyErr = fmt.Errorf("the rollout plan has to start from zero, partition= %d", *c.rolloutSpec.BatchPartition)
+ c.rolloutStatus.RolloutFailed(verifyErr.Error())
+ return c.rolloutStatus
+ }
+
+ // the number of old version in the Cloneset equals to the total number
+ oldVersionPod, _ := intstr.GetValueFromIntOrPercent(c.cloneSet.Spec.UpdateStrategy.Partition, int(totalReplicas),
+ true)
+ if oldVersionPod != int(totalReplicas) {
+ verifyErr = fmt.Errorf("the cloneset was still in the middle of updating, number of old pods= %d", oldVersionPod)
+ c.rolloutStatus.RolloutFailed(verifyErr.Error())
+ return c.rolloutStatus
+ }
+
+ // mark the rollout verified
+ c.recorder.Event(c.parentController, event.Normal("Verified",
+ "Rollout spec and the CloneSet resource are verified"))
+ c.rolloutStatus.StateTransition(v1alpha1.RollingSpecVerifiedEvent)
+ return c.rolloutStatus
+}
+
+// Initialize makes sure that
+func (c *CloneSetController) Initialize(ctx context.Context) *v1alpha1.RolloutStatus {
+ if c.fetchCloneSet(ctx) != nil {
+ return c.rolloutStatus
+ }
+
+ // mark the rollout initialized, there is nothing we need to do for Cloneset for now
+ c.recorder.Event(c.parentController, event.Normal("Initialized", "Rollout resource are initialized"))
+ c.rolloutStatus.StateTransition(v1alpha1.RollingInitializedEvent)
+ return c.rolloutStatus
+}
+
+// RolloutOneBatchPods calculates the number of pods we can upgrade once according to the rollout spec
+// and then set the partition accordingly
+func (c *CloneSetController) RolloutOneBatchPods(ctx context.Context) *v1alpha1.RolloutStatus {
+ // calculate what's the total pods that should be upgraded given the currentBatch in the status
+ cloneSetSize, _ := c.Size(ctx)
+ newPodTarget := c.calculateNewPodTarget(int(cloneSetSize))
+ // set the Partition as the desired number of pods in old revisions.
+ clonePatch := client.MergeFrom(c.cloneSet.DeepCopyObject())
+ c.cloneSet.Spec.UpdateStrategy.Partition = &intstr.IntOrString{Type: intstr.Int,
+ IntVal: cloneSetSize - int32(newPodTarget)}
+ // patch the Cloneset
+ if err := c.client.Patch(ctx, c.cloneSet, clonePatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
+ c.recorder.Event(c.parentController, event.Warning("Failed to patch update the Cloneset", err))
+ c.rolloutStatus.RolloutRetry(err.Error())
+ return c.rolloutStatus
+ }
+ // record the upgrade
+ klog.InfoS("upgraded one batch", "current batch", c.rolloutStatus.CurrentBatch)
+ c.recorder.Event(c.parentController, event.Normal("Rollout",
+ fmt.Sprintf("upgraded the batch num = %d", c.rolloutStatus.CurrentBatch)))
+ c.rolloutStatus.StateTransition(v1alpha1.BatchRolloutVerifyingEvent)
+ c.rolloutStatus.UpgradedReplicas = int32(newPodTarget)
+ return c.rolloutStatus
+}
+
+// CheckOneBatchPods checks to see if the pods are all available according to
+func (c *CloneSetController) CheckOneBatchPods(ctx context.Context) *v1alpha1.RolloutStatus {
+ cloneSetSize, _ := c.Size(ctx)
+ newPodTarget := c.calculateNewPodTarget(int(cloneSetSize))
+ // get the number of ready pod from cloneset
+ readyPodCount := int(c.cloneSet.Status.UpdatedReadyReplicas)
+ currentBatch := c.rolloutSpec.RolloutBatches[c.rolloutStatus.CurrentBatch]
+ unavail := 0
+ if currentBatch.MaxUnavailable != nil {
+ unavail, _ = intstr.GetValueFromIntOrPercent(currentBatch.MaxUnavailable, int(cloneSetSize), true)
+ }
+ klog.V(common.LogDebug).InfoS("checking the rolling out progress", "current batch", currentBatch,
+ "new pod count target", newPodTarget, "new ready pod count", readyPodCount,
+ "max unavailable pod allowed", unavail)
+ c.rolloutStatus.UpgradedReadyReplicas = int32(readyPodCount)
+ if unavail+readyPodCount >= newPodTarget {
+ // record the successful upgrade
+ klog.InfoS("pods are ready", "current batch", currentBatch)
+ c.recorder.Event(c.parentController, event.Normal("Batch Available",
+ fmt.Sprintf("the batch num = %d is available", c.rolloutStatus.CurrentBatch)))
+ c.rolloutStatus.StateTransition(v1alpha1.OneBatchAvailableEvent)
+ } else {
+ // continue to verify
+ klog.V(common.LogDebug).InfoS("the batch is not ready yet", "current batch", currentBatch)
+ c.rolloutStatus.StateTransition(v1alpha1.BatchRolloutVerifyingEvent)
+ }
+ return c.rolloutStatus
+}
+
+// FinalizeOneBatch makes sure that the rollout status are updated correctly
+func (c *CloneSetController) FinalizeOneBatch(ctx context.Context) *v1alpha1.RolloutStatus {
+ // nothing to do for now
+ return c.rolloutStatus
+}
+
+// Finalize makes sure the Cloneset is all upgraded
+func (c *CloneSetController) Finalize(ctx context.Context) *v1alpha1.RolloutStatus {
+ if c.fetchCloneSet(ctx) != nil {
+ return c.rolloutStatus
+ }
+
+ c.rolloutStatus.StateTransition(v1alpha1.RollingFinalizedEvent)
+
+ return c.rolloutStatus
+}
+
+/* --------------------
+The functions below are helper functions
+--------------------- */
+// check if the replicas in all the rollout batches add up to the right number
+func (c *CloneSetController) verifyBatchSizes(totalReplicas int32) error {
+ // the target size has to be the same as the cloneset size
+ if c.rolloutSpec.TargetSize != nil && *c.rolloutSpec.TargetSize != totalReplicas {
+ return fmt.Errorf("the rollout plan is attempting to scale the cloneset, target = %d, cloneset size = %d",
+ *c.rolloutSpec.TargetSize, totalReplicas)
+ }
+ // use a common function to check if the sum of all the batches can match the cloneset size
+ err := VerifySumOfBatchSizes(c.rolloutSpec, totalReplicas)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (c *CloneSetController) fetchCloneSet(ctx context.Context) error {
+ // get the cloneSet
+ workload := kruise.CloneSet{}
+ err := c.client.Get(ctx, c.workloadNamespacedName, &workload)
+ if err != nil {
+ if !apierrors.IsNotFound(err) {
+ c.recorder.Event(c.parentController, event.Warning("Failed to get the Cloneset", err))
+ }
+ c.rolloutStatus.RolloutRetry(err.Error())
+ return err
+ }
+ c.cloneSet = &workload
+ return nil
+}
+
+func (c *CloneSetController) calculateNewPodTarget(cloneSetSize int) int {
+ currentBatch := int(c.rolloutStatus.CurrentBatch)
+ newPodTarget := 0
+ if currentBatch == len(c.rolloutSpec.RolloutBatches)-1 {
+ // special handle the last batch, we ignore the rest of the batch in case there are rounding errors
+ klog.InfoS("use the cloneset size as the total pod target for the last rolling batch",
+ "current batch", currentBatch, "new version pod target", newPodTarget)
+ newPodTarget = cloneSetSize
+ } else {
+ for i, r := range c.rolloutSpec.RolloutBatches {
+ batchSize, _ := intstr.GetValueFromIntOrPercent(&r.Replicas, cloneSetSize, true)
+ if i <= currentBatch {
+ newPodTarget += batchSize
+ } else {
+ break
+ }
+ }
+ klog.InfoS("Calculated the number of new version pod", "current batch", currentBatch,
+ "new version pod target", newPodTarget)
+ }
+ return newPodTarget
+}
diff --git a/pkg/controller/common/rollout/workloads/common.go b/pkg/controller/common/rollout/workloads/common.go
new file mode 100644
index 000000000..0d66b4c84
--- /dev/null
+++ b/pkg/controller/common/rollout/workloads/common.go
@@ -0,0 +1,39 @@
+package workloads
+
+import (
+ "fmt"
+
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
+)
+
+// VerifySumOfBatchSizes verifies that the the sum of all the batch replicas is valid given the total replica
+// each batch replica can be absolute or a percentage
+func VerifySumOfBatchSizes(rolloutSpec *v1alpha1.RolloutPlan, totalReplicas int32) error {
+ // if not set, the sum of all the batch sizes minus the last batch cannot be more than the totalReplicas
+ // if not set, the sum of all the batch sizes minus the last batch cannot be more than the totalReplicas
+ totalRollout := 0
+ for i := 0; i < len(rolloutSpec.RolloutBatches)-1; i++ {
+ rb := rolloutSpec.RolloutBatches[i]
+ batchSize, _ := intstr.GetValueFromIntOrPercent(&rb.Replicas, int(totalReplicas), true)
+ totalRollout += batchSize
+ }
+ if totalRollout >= int(totalReplicas) {
+ return fmt.Errorf("the rollout plan batch size mismatch, total batch size = %d, totalReplicas size = %d",
+ totalRollout, totalReplicas)
+ }
+
+ // include the last batch if it has an int value
+ // we ignore the last batch percentage since it is very likely to cause rounding errors
+ lastBatch := rolloutSpec.RolloutBatches[len(rolloutSpec.RolloutBatches)-1]
+ if lastBatch.Replicas.Type == intstr.Int {
+ totalRollout += int(lastBatch.Replicas.IntVal)
+ // now that they should be the same
+ if totalRollout != int(totalReplicas) {
+ return fmt.Errorf("the rollout plan batch size mismatch, total batch size = %d, totalReplicas size = %d",
+ totalRollout, totalReplicas)
+ }
+ }
+ return nil
+}
diff --git a/pkg/controller/common/rollout/workloads/controller.go b/pkg/controller/common/rollout/workloads/controller.go
new file mode 100644
index 000000000..99f9da3fb
--- /dev/null
+++ b/pkg/controller/common/rollout/workloads/controller.go
@@ -0,0 +1,41 @@
+package workloads
+
+import (
+ "context"
+
+ "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
+)
+
+// WorkloadController is the interface that all type of cloneSet controller implements
+type WorkloadController interface {
+ // Size returns the total number of pods in the resources according to the spec
+ Size(ctx context.Context) (int32, error)
+
+ // Verify makes sure that the resources can be upgraded according to the rollout plan
+ // it returns new rollout status
+ Verify(ctx context.Context) *v1alpha1.RolloutStatus
+
+ // Initialize make sure that the resource is ready to be upgraded.
+ Initialize(ctx context.Context) *v1alpha1.RolloutStatus
+
+ // RolloutOneBatchPods tries to upgrade pods in the resources following the rollout plan
+ // it will upgrade as many pods as the rollout plan allows at once, the routine does not block on any operations.
+ // Instead, we rely on the go-client's requeue mechanism to drive this towards the spec goal
+ // it returns the number of pods upgraded in this round
+ RolloutOneBatchPods(ctx context.Context) *v1alpha1.RolloutStatus
+
+ // CheckOneBatchPods tries to upgrade pods in the resources following the rollout plan
+ // it will upgrade as many pods as the rollout plan allows at once, the routine does not block on any operations.
+ // Instead, we rely on the go-client's requeue mechanism to drive this towards the spec goal
+ // it returns the number of pods upgraded in this round
+ CheckOneBatchPods(ctx context.Context) *v1alpha1.RolloutStatus
+
+ // FinalizeOneBatch makes sure that the rollout can start the next batch
+ // it also needs to handle the corner cases around the very last batch
+ FinalizeOneBatch(ctx context.Context) *v1alpha1.RolloutStatus
+
+ // Finalize makes sure the resources are in a good final state.
+ // For example, we may remove the source object to prevent scalar traits to ever work
+ // and we will call the finalize rollout web hooks
+ Finalize(ctx context.Context) *v1alpha1.RolloutStatus
+}
diff --git a/pkg/controller/core.oam.dev/oamruntime_controller.go b/pkg/controller/core.oam.dev/oamruntime_controller.go
index b5b81f275..7fd960ab3 100644
--- a/pkg/controller/core.oam.dev/oamruntime_controller.go
+++ b/pkg/controller/core.oam.dev/oamruntime_controller.go
@@ -16,15 +16,33 @@ limitations under the License.
package core_oam_dev
+// ApplyOnceOnlyMode enumerates ApplyOnceOnly modes.
+type ApplyOnceOnlyMode string
+
+const (
+ // ApplyOnceOnlyOff indicates workloads and traits should always be affected.
+ // It means ApplyOnceOnly is disabled.
+ ApplyOnceOnlyOff ApplyOnceOnlyMode = "off"
+
+ // ApplyOnceOnlyOn indicates workloads and traits should not be affected
+ // if no spec change is made in the ApplicationConfiguration.
+ ApplyOnceOnlyOn = "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"
+)
+
// Args args used by controller
type Args struct {
// RevisionLimit is the maximum number of revisions that will be maintained.
// The default value is 50.
RevisionLimit int
- // ApplyOnceOnly indicates whether workloads and traits should be
+ // ApplyMode indicates whether workloads and traits should be
// affected if no spec change is made in the ApplicationConfiguration.
- ApplyOnceOnly bool
+ ApplyMode ApplyOnceOnlyMode
// 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
diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go
index 29050fa9d..3792118fc 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go
@@ -24,8 +24,11 @@ import (
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/go-logr/logr"
+ "github.com/pkg/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -76,13 +79,13 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
app.Status.Phase = v1alpha2.ApplicationRollingOut
app.Status.SetConditions(readyCondition("Rolling"))
// do not process apps still in rolling out
- return ctrl.Result{RequeueAfter: RolloutReconcileWaitTime}, r.Status().Update(ctx, app)
+ return ctrl.Result{RequeueAfter: RolloutReconcileWaitTime}, r.UpdateStatus(ctx, app)
}
applog.Info("Start Rendering")
app.Status.Phase = v1alpha2.ApplicationRendering
- handler := &reter{r.Client, app, applog}
+ handler := &appHandler{r, app, applog}
app.Status.Conditions = []v1alpha1.Condition{}
@@ -109,9 +112,8 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
app.Status.SetConditions(readyCondition("Built"))
-
- applog.Info("apply applicationconfig & component to the cluster")
- // apply applicationconfig & component to the cluster
+ applog.Info("apply appConfig & component to the cluster")
+ // apply appConfig & component to the cluster
if err := handler.apply(ctx, ac, comps); err != nil {
handler.l.Error(err, "[Handle apply]")
app.Status.SetConditions(errorCondition("Applied", err))
@@ -120,13 +122,22 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
app.Status.SetConditions(readyCondition("Applied"))
+ app.Status.Phase = v1alpha2.ApplicationHealthChecking
applog.Info("check application health status")
// check application health status
- if err := handler.healthCheck(appfile); err != nil {
+ appCompStatus, healthy, err := handler.statusAggregate(appfile)
+ if err != nil {
app.Status.SetConditions(errorCondition("HealthCheck", err))
return handler.Err(err)
}
+ if !healthy {
+ app.Status.SetConditions(errorCondition("HealthCheck", errors.New("not healthy")))
+ app.Status.Services = appCompStatus
+ // unhealthy will check again after 10s
+ return ctrl.Result{RequeueAfter: time.Second * 10}, r.Status().Update(ctx, app)
+ }
+ app.Status.Services = appCompStatus
app.Status.SetConditions(readyCondition("HealthCheck"))
app.Status.Phase = v1alpha2.ApplicationRunning
// Gather status of components
@@ -140,7 +151,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
})
}
app.Status.Components = refComps
- return ctrl.Result{}, r.Status().Update(ctx, app)
+ return ctrl.Result{}, r.UpdateStatus(ctx, app)
}
// SetupWithManager install to manager
@@ -151,6 +162,18 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
Complete(r)
}
+// UpdateStatus updates v1alpha2.Application's Status with retry.RetryOnConflict
+func (r *Reconciler) UpdateStatus(ctx context.Context, app *v1alpha2.Application, opts ...client.UpdateOption) error {
+ status := app.DeepCopy().Status
+ return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
+ if err = r.Get(ctx, types.NamespacedName{Namespace: app.Namespace, Name: app.Name}, app); err != nil {
+ return
+ }
+ app.Status = status
+ return r.Status().Update(ctx, app, opts...)
+ })
+}
+
// Setup adds a controller that reconciles ApplicationDeployment.
func Setup(mgr ctrl.Manager, _ core.Args, _ logging.Logger) error {
dm, err := discoverymapper.New(mgr.GetConfig())
diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go
index 7559c2572..02a9fad42 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go
@@ -25,12 +25,11 @@ import (
"net/http/httptest"
"time"
- "github.com/stretchr/testify/assert"
-
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/google/go-cmp/cmp"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
+ "github.com/stretchr/testify/assert"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -87,7 +86,7 @@ var _ = Describe("Test Application Controller", func() {
},
}
- var getExpDeployment = func(compName string) *v1.Deployment {
+ var getExpDeployment = func(compName, appName string) *v1.Deployment {
return &v1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
@@ -96,6 +95,8 @@ var _ = Describe("Test Application Controller", func() {
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"workload.oam.dev/type": "worker",
+ "app.oam.dev/component": compName,
+ "app.oam.dev/name": appName,
},
},
Spec: v1.DeploymentSpec{
@@ -125,18 +126,23 @@ var _ = Describe("Test Application Controller", func() {
},
}
appWithTrait.Spec.Components[0].Name = "myweb3"
- expectScalerTrait := unstructured.Unstructured{Object: map[string]interface{}{
- "apiVersion": "core.oam.dev/v1alpha2",
- "kind": "ManualScalerTrait",
- "metadata": map[string]interface{}{
- "labels": map[string]interface{}{
- "trait.oam.dev/type": "scaler",
+ expectScalerTrait := func(compName, appName string) unstructured.Unstructured {
+ return unstructured.Unstructured{Object: map[string]interface{}{
+ "apiVersion": "core.oam.dev/v1alpha2",
+ "kind": "ManualScalerTrait",
+ "metadata": map[string]interface{}{
+ "labels": map[string]interface{}{
+ "trait.oam.dev/type": "scaler",
+ "app.oam.dev/component": compName,
+ "app.oam.dev/name": appName,
+ },
},
- },
- "spec": map[string]interface{}{
- "replicaCount": int64(2),
- },
- }}
+ "spec": map[string]interface{}{
+ "replicaCount": int64(2),
+ },
+ }}
+ }
+
appWithTraitAndScope := appWithTrait.DeepCopy()
appWithTraitAndScope.SetName("app-with-trait-and-scope")
appWithTraitAndScope.Spec.Components[0].Scopes = map[string]string{"healthscopes.core.oam.dev": "appWithTraitAndScope-default-health"}
@@ -189,7 +195,7 @@ var _ = Describe("Test Application Controller", func() {
})
It("app-without-trait will only create workload", func() {
- expDeployment := getExpDeployment("myweb2")
+ expDeployment := getExpDeployment("myweb2", appwithNoTrait.Name)
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vela-test",
@@ -230,13 +236,14 @@ var _ = Describe("Test Application Controller", func() {
gotD := &v1.Deployment{}
Expect(json.Unmarshal(component.Spec.Workload.Raw, gotD)).Should(BeNil())
- Expect(gotD).Should(BeEquivalentTo(expDeployment))
+ fmt.Println(cmp.Diff(expDeployment, gotD))
+ Expect(assert.ObjectsAreEqual(expDeployment, gotD)).Should(BeEquivalentTo(true))
By("Delete Application, clean the resource")
Expect(k8sClient.Delete(ctx, appwithNoTrait)).Should(BeNil())
})
It("app-with-config will create workload with config data", func() {
- expConfigDeployment := getExpDeployment("myweb1")
+ expConfigDeployment := getExpDeployment("myweb1", appwithConfig.Name)
expConfigDeployment.SetAnnotations(map[string]string{"c1": "v1", "c2": "v2"})
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
@@ -283,7 +290,7 @@ var _ = Describe("Test Application Controller", func() {
})
It("app-with-trait will create workload and trait", func() {
- expDeployment := getExpDeployment("myweb3")
+ expDeployment := getExpDeployment("myweb3", appWithTrait.Name)
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vela-test-with-trait",
@@ -314,7 +321,7 @@ var _ = Describe("Test Application Controller", func() {
gotTrait := unstructured.Unstructured{}
Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[0].Trait.Raw, &gotTrait)).Should(BeNil())
- Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait))
+ Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait("myweb3", app.Name)))
By("Check component created as expected")
component := &v1alpha2.Component{}
@@ -336,13 +343,14 @@ var _ = Describe("Test Application Controller", func() {
It("app-with-composedworkload-trait will create workload and trait", func() {
compName := "myweb-composed-3"
- expDeployment := getExpDeployment(compName)
+ var appname = "app-with-composedworkload-trait"
+ expDeployment := getExpDeployment(compName, appname)
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vela-test-with-composedworkload-trait",
},
}
- var appname = "app-with-composedworkload-trait"
+
appWithComposedWorkload := appwithNoTrait.DeepCopy()
appWithComposedWorkload.Spec.Components[0].WorkloadType = "webserver"
appWithComposedWorkload.SetName(appname)
@@ -384,7 +392,12 @@ var _ = Describe("Test Application Controller", func() {
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
- "labels": map[string]interface{}{"trait.oam.dev/type": "AuxiliaryWorkload"},
+ "labels": map[string]interface{}{
+ "trait.oam.dev/type": "AuxiliaryWorkload",
+ "app.oam.dev/name": "app-with-composedworkload-trait",
+ "app.oam.dev/component": "myweb-composed-3",
+ "trait.oam.dev/resource": "service",
+ },
},
"spec": map[string]interface{}{
"ports": []interface{}{
@@ -402,7 +415,7 @@ var _ = Describe("Test Application Controller", func() {
By("Check the second trait should be scaler")
gotTrait = unstructured.Unstructured{}
Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[1].Trait.Raw, &gotTrait)).Should(BeNil())
- Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait))
+ Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait("myweb-composed-3", app.Name)))
By("Check component created as expected")
component := &v1alpha2.Component{}
@@ -426,7 +439,7 @@ var _ = Describe("Test Application Controller", func() {
})
It("app-with-trait-and-scope will create workload, trait and scope", func() {
- expDeployment := getExpDeployment("myweb4")
+ expDeployment := getExpDeployment("myweb4", appWithTraitAndScope.Name)
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vela-test-with-trait-scope",
@@ -457,7 +470,7 @@ var _ = Describe("Test Application Controller", func() {
gotTrait := unstructured.Unstructured{}
Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[0].Trait.Raw, &gotTrait)).Should(BeNil())
- Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait))
+ Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait("myweb4", app.Name)))
Expect(appConfig.Spec.Components[0].Scopes[0].ScopeReference).Should(BeEquivalentTo(v1alpha1.TypedReference{
APIVersion: "core.oam.dev/v1alpha2",
@@ -484,7 +497,7 @@ var _ = Describe("Test Application Controller", func() {
})
It("app with two components and update", func() {
- expDeployment := getExpDeployment("myweb5")
+ expDeployment := getExpDeployment("myweb5", appWithTwoComp.Name)
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "app-with-two-comps",
@@ -519,7 +532,7 @@ var _ = Describe("Test Application Controller", func() {
gotTrait := unstructured.Unstructured{}
Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[0].Trait.Raw, &gotTrait)).Should(BeNil())
- Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait))
+ Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait("myweb5", app.Name)))
Expect(appConfig.Spec.Components[0].Scopes[0].ScopeReference).Should(BeEquivalentTo(v1alpha1.TypedReference{
APIVersion: "core.oam.dev/v1alpha2",
@@ -543,7 +556,7 @@ var _ = Describe("Test Application Controller", func() {
Expect(json.Unmarshal(component5.Spec.Workload.Raw, gotD)).Should(BeNil())
Expect(gotD).Should(BeEquivalentTo(expDeployment))
- expDeployment6 := getExpDeployment("myweb6")
+ expDeployment6 := getExpDeployment("myweb6", app.Name)
expDeployment6.SetAnnotations(map[string]string{"c1": "v1", "c2": "v2"})
expDeployment6.Spec.Template.Spec.Containers[0].Image = "busybox2"
component6 := &v1alpha2.Component{}
@@ -586,7 +599,7 @@ var _ = Describe("Test Application Controller", func() {
}, appConfig)).Should(BeNil())
Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[0].Trait.Raw, &gotTrait)).Should(BeNil())
- Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait))
+ Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait("myweb5", app.Name)))
Expect(appConfig.Spec.Components[0].Scopes[0].ScopeReference).Should(BeEquivalentTo(v1alpha1.TypedReference{
APIVersion: "core.oam.dev/v1alpha2",
@@ -608,7 +621,7 @@ var _ = Describe("Test Application Controller", func() {
expDeployment.Spec.Template.Spec.Containers[0].Image = "busybox3"
Expect(gotD).Should(BeEquivalentTo(expDeployment))
- expDeployment7 := getExpDeployment("myweb7")
+ expDeployment7 := getExpDeployment("myweb7", app.Name)
component7 := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: app.Namespace,
@@ -631,7 +644,8 @@ var _ = Describe("Test Application Controller", func() {
It("app-with-trait will create workload and trait with http task", func() {
s := NewMock()
defer s.Close()
- expectScalerTrait.Object["spec"].(map[string]interface{})["token"] = "test-token"
+ expTrait := expectScalerTrait(appWithTrait.Spec.Components[0].Name, appWithTrait.Name)
+ expTrait.Object["spec"].(map[string]interface{})["token"] = "test-token"
By("change trait definition with http task")
ntd, otd := &v1alpha2.TraitDefinition{}, &v1alpha2.TraitDefinition{}
@@ -671,7 +685,7 @@ var _ = Describe("Test Application Controller", func() {
gotTrait := unstructured.Unstructured{}
Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[0].Trait.Raw, &gotTrait)).Should(BeNil())
- Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait))
+ Expect(gotTrait).Should(BeEquivalentTo(expTrait))
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
})
@@ -690,8 +704,10 @@ var _ = Describe("Test Application Controller", func() {
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "scaler"}, otd)).Should(BeNil())
ntd.ResourceVersion = otd.ResourceVersion
Expect(k8sClient.Update(ctx, ntd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
+ compName := "myweb-health"
+ expDeployment := getExpDeployment(compName, appWithTrait.Name)
- expDeployment := getExpDeployment("myweb6")
+ By("create the new namespace")
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vela-test-with-health",
@@ -699,23 +715,29 @@ var _ = Describe("Test Application Controller", func() {
}
appWithTrait.SetNamespace(ns.Name)
Expect(k8sClient.Create(ctx, ns)).Should(BeNil())
+
app := appWithTrait.DeepCopy()
+ app.Spec.Components[0].Name = compName
expDeployment.Name = app.Name
expDeployment.Namespace = ns.Name
expDeployment.Labels[oam.LabelAppName] = app.Name
+ expDeployment.Labels[oam.LabelAppComponent] = compName
+ expDeployment.Labels["app.oam.dev/resourceType"] = "WORKLOAD"
Expect(k8sClient.Create(ctx, expDeployment)).Should(BeNil())
- expectScalerTrait.SetName(app.Name)
- expectScalerTrait.SetNamespace(app.Namespace)
- expectScalerTrait.SetLabels(map[string]string{
- oam.LabelAppName: app.Name,
- "trait.oam.dev/type": "scaler",
+ expTrait := expectScalerTrait(compName, app.Name)
+ expTrait.SetName(app.Name)
+ expTrait.SetNamespace(app.Namespace)
+ expTrait.SetLabels(map[string]string{
+ oam.LabelAppName: app.Name,
+ "trait.oam.dev/type": "scaler",
+ "app.oam.dev/component": "myweb-health",
})
- (expectScalerTrait.Object["spec"].(map[string]interface{}))["workloadRef"] = map[string]interface{}{
+ (expTrait.Object["spec"].(map[string]interface{}))["workloadRef"] = map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"name": app.Name,
}
- Expect(k8sClient.Create(ctx, &expectScalerTrait)).Should(BeNil())
+ Expect(k8sClient.Create(ctx, &expTrait)).Should(BeNil())
By("enrich the status of deployment and scaler trait")
expDeployment.Status.Replicas = 1
@@ -726,13 +748,13 @@ var _ = Describe("Test Application Controller", func() {
Namespace: app.Namespace,
Name: app.Name,
}, got)).Should(BeNil())
- expectScalerTrait.Object["status"] = v1alpha1.ConditionedStatus{
+ expTrait.Object["status"] = v1alpha1.ConditionedStatus{
Conditions: []v1alpha1.Condition{{
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
}},
}
- Expect(k8sClient.Status().Update(ctx, &expectScalerTrait)).Should(BeNil())
+ Expect(k8sClient.Status().Update(ctx, &expTrait)).Should(BeNil())
tGot := &unstructured.Unstructured{}
tGot.SetAPIVersion("core.oam.dev/v1alpha2")
tGot.SetKind("ManualScalerTrait")
@@ -750,15 +772,28 @@ var _ = Describe("Test Application Controller", func() {
reconcileRetry(reconciler, reconcile.Request{NamespacedName: appKey})
By("Check App running successfully")
- checkApp := &v1alpha2.Application{}
- Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
- Expect(checkApp.Status.Phase).Should(Equal(v1alpha2.ApplicationRunning))
+
+ Eventually(func() string {
+ _, err := reconciler.Reconcile(reconcile.Request{NamespacedName: appKey})
+ if err != nil {
+ return err.Error()
+ }
+ checkApp := &v1alpha2.Application{}
+ err = k8sClient.Get(ctx, appKey, checkApp)
+ if err != nil {
+ return err.Error()
+ }
+ if checkApp.Status.Phase != v1alpha2.ApplicationRunning {
+ fmt.Println(checkApp.Status.Conditions)
+ }
+ return string(checkApp.Status.Phase)
+ }(), 5*time.Second, time.Second).Should(BeEquivalentTo(v1alpha2.ApplicationRunning))
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
})
It("app with rolling out annotation", func() {
- By("crreat application with rolling out annotation")
+ By("create application with rolling out annotation")
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "app-test-with-rollout",
@@ -787,11 +822,170 @@ var _ = Describe("Test Application Controller", func() {
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
})
+
+ It("app with health policy and custom status for workload", func() {
+ By("change workload and trait definition with health policy")
+ nwd := &v1alpha2.WorkloadDefinition{}
+ wDDefJson, _ := yaml.YAMLToJSON([]byte(wdDefWithHealthStatusYaml))
+ Expect(json.Unmarshal(wDDefJson, nwd)).Should(BeNil())
+ Expect(k8sClient.Create(ctx, nwd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
+ ntd := &v1alpha2.TraitDefinition{}
+ tDDefJson, _ := yaml.YAMLToJSON([]byte(tDDefWithHealthStatusYaml))
+ Expect(json.Unmarshal(tDDefJson, ntd)).Should(BeNil())
+ Expect(k8sClient.Create(ctx, ntd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
+ compName := "myweb-health-status"
+ appWithTraitHealthStatus := appWithTrait.DeepCopy()
+ appWithTraitHealthStatus.Name = "app-trait-health-status"
+ expDeployment := getExpDeployment(compName, appWithTraitHealthStatus.Name)
+
+ By("create the new namespace")
+ ns := &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "vela-test-with-health-status",
+ },
+ }
+ appWithTraitHealthStatus.SetNamespace(ns.Name)
+ Expect(k8sClient.Create(ctx, ns)).Should(BeNil())
+
+ app := appWithTraitHealthStatus.DeepCopy()
+ app.Spec.Components[0].Name = compName
+ app.Spec.Components[0].WorkloadType = "nworker"
+ app.Spec.Components[0].Settings = runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox3","lives":"3","enemies":"alain"}`)}
+ app.Spec.Components[0].Traits[0].Name = "ingress"
+ app.Spec.Components[0].Traits[0].Properties = runtime.RawExtension{Raw: []byte(`{"domain":"example.com","http":{"/":80}}`)}
+
+ expDeployment.Name = app.Name
+ expDeployment.Namespace = ns.Name
+ expDeployment.Labels[oam.LabelAppName] = app.Name
+ expDeployment.Labels[oam.LabelAppComponent] = compName
+ expDeployment.Labels["app.oam.dev/resourceType"] = "WORKLOAD"
+ Expect(k8sClient.Create(ctx, expDeployment)).Should(BeNil())
+
+ expWorkloadTrait := unstructured.Unstructured{Object: map[string]interface{}{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]interface{}{
+ "labels": map[string]interface{}{
+ "trait.oam.dev/type": "AuxiliaryWorkload",
+ "app.oam.dev/component": compName,
+ "app.oam.dev/name": app.Name,
+ "trait.oam.dev/resource": "gameconfig",
+ },
+ },
+ "data": map[string]interface{}{
+ "enemies": "alien",
+ "lives": "3",
+ },
+ }}
+ expWorkloadTrait.SetName("myweb-health-statusgame-config")
+ expWorkloadTrait.SetNamespace(app.Namespace)
+ Expect(k8sClient.Create(ctx, &expWorkloadTrait)).Should(BeNil())
+
+ expTrait := unstructured.Unstructured{Object: map[string]interface{}{
+ "apiVersion": "networking.k8s.io/v1beta1",
+ "kind": "Ingress",
+ "metadata": map[string]interface{}{
+ "labels": map[string]interface{}{
+ "trait.oam.dev/type": "ingress",
+ "trait.oam.dev/resource": "ingress",
+ "app.oam.dev/component": compName,
+ "app.oam.dev/name": app.Name,
+ },
+ },
+ "spec": map[string]interface{}{
+ "rules": []interface{}{
+ map[string]interface{}{
+ "host": "example.com",
+ },
+ },
+ },
+ }}
+ expTrait.SetName(compName)
+ expTrait.SetNamespace(app.Namespace)
+ Expect(k8sClient.Create(ctx, &expTrait)).Should(BeNil())
+
+ expTrait2 := unstructured.Unstructured{Object: map[string]interface{}{
+ "apiVersion": "v1",
+ "kind": "Service",
+ "metadata": map[string]interface{}{
+ "labels": map[string]interface{}{
+ "trait.oam.dev/type": "ingress",
+ "trait.oam.dev/resource": "service",
+ "app.oam.dev/component": compName,
+ "app.oam.dev/name": app.Name,
+ },
+ },
+ "spec": map[string]interface{}{
+ "clusterIP": "10.0.0.4",
+ "ports": []interface{}{
+ map[string]interface{}{
+ "port": 80,
+ },
+ },
+ },
+ }}
+ expTrait2.SetName(app.Name)
+ expTrait2.SetNamespace(app.Namespace)
+ Expect(k8sClient.Create(ctx, &expTrait2)).Should(BeNil())
+
+ By("enrich the status of deployment and ingress trait")
+ expDeployment.Status.Replicas = 1
+ expDeployment.Status.ReadyReplicas = 1
+ Expect(k8sClient.Status().Update(ctx, expDeployment)).Should(BeNil())
+ got := &v1.Deployment{}
+ Expect(k8sClient.Get(ctx, client.ObjectKey{
+ Namespace: app.Namespace,
+ Name: app.Name,
+ }, got)).Should(BeNil())
+
+ By("apply appfile")
+ Expect(k8sClient.Create(ctx, app)).Should(BeNil())
+ appKey := client.ObjectKey{
+ Name: app.Name,
+ Namespace: app.Namespace,
+ }
+ reconcileRetry(reconciler, reconcile.Request{NamespacedName: appKey})
+
+ By("Check App running successfully")
+ checkApp := &v1alpha2.Application{}
+ Eventually(func() string {
+ _, err := reconciler.Reconcile(reconcile.Request{NamespacedName: appKey})
+ if err != nil {
+ return err.Error()
+ }
+ err = k8sClient.Get(ctx, appKey, checkApp)
+ if err != nil {
+ return err.Error()
+ }
+ if checkApp.Status.Phase != v1alpha2.ApplicationRunning {
+ fmt.Println(checkApp.Status.Conditions)
+ }
+ return string(checkApp.Status.Phase)
+ }(), 5*time.Second, time.Second).Should(BeEquivalentTo(v1alpha2.ApplicationRunning))
+ Expect(checkApp.Status.Services).Should(BeEquivalentTo([]v1alpha2.ApplicationComponentStatus{
+ {
+ Name: compName,
+ Healthy: true,
+ Message: "type: busybox,\t enemies:alien",
+ Traits: []v1alpha2.ApplicationTraitStatus{
+ {
+ Type: "ingress",
+ Healthy: true,
+ Message: "type: ClusterIP,\t clusterIP:10.0.0.4,\t ports:80,\t domainexample.com",
+ },
+ },
+ },
+ }))
+ Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
+ })
})
func reconcileRetry(r reconcile.Reconciler, req reconcile.Request) {
Eventually(func() error {
_, err := r.Reconcile(req)
+ if err != nil {
+ fmt.Println("reconcile err: ", err)
+ }
return err
}, 3*time.Second, time.Second).Should(BeNil())
}
@@ -970,7 +1164,7 @@ spec:
name: deployments.apps
extension:
healthPolicy: |
- isHealth: output.status.readyReplicas == output.status.replicas
+ isHealth: context.output.status.readyReplicas == context.output.status.replicas
template: |
output: {
apiVersion: "apps/v1"
@@ -1018,6 +1212,72 @@ spec:
cmd?: [...string]
}
+`
+ wdDefWithHealthStatusYaml = `apiVersion: core.oam.dev/v1alpha2
+kind: WorkloadDefinition
+metadata:
+ name: nworker
+ 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
+ status:
+ healthPolicy: |
+ 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
+ }
+
+ 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
+ }
+ }]
+ }
+ }
+ }
+ }
+
+ 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
+ }
`
tDDefYaml = `
apiVersion: core.oam.dev/v1alpha2
@@ -1108,7 +1368,7 @@ spec:
workloadRefPath: spec.workloadRef
extension:
healthPolicy: |
- isHealth: output.status.conditions[0].status == "True"
+ isHealth: context.output.status.conditions[0].status == "True"
template: |-
output: {
apiVersion: "core.oam.dev/v1alpha2"
@@ -1122,6 +1382,60 @@ spec:
replicas: *1 | int
}
`
+
+ tDDefWithHealthStatusYaml = `apiVersion: core.oam.dev/v1alpha2
+kind: TraitDefinition
+metadata:
+ name: ingress
+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
+ }
+ },
+ ]
+ }
+ }]
+ }
+ }
+`
)
func NewMock() *httptest.Server {
diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go
index 164fc441d..cce108aa9 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go
@@ -6,6 +6,7 @@ import (
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/go-logr/logr"
+ "github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -38,14 +39,14 @@ func readyCondition(tpy string) runtimev1alpha1.Condition {
}
}
-type reter struct {
- c client.Client
+type appHandler struct {
+ r *Reconciler
app *v1alpha2.Application
l logr.Logger
}
-func (ret *reter) Err(err error) (ctrl.Result, error) {
- nerr := ret.c.Status().Update(context.Background(), ret.app)
+func (ret *appHandler) Err(err error) (ctrl.Result, error) {
+ nerr := ret.r.UpdateStatus(context.Background(), ret.app)
if err == nil && nerr == nil {
return ctrl.Result{}, nil
}
@@ -57,8 +58,8 @@ func (ret *reter) Err(err error) (ctrl.Result, error) {
}, nil
}
-func (ret *reter) apply(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, comps []*v1alpha2.Component) error {
- // set ownerReference for ApplicationConfiguration and Components created by Application
+// apply will set ownerReference for ApplicationConfiguration and Components created by Application
+func (ret *appHandler) apply(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, comps []*v1alpha2.Component) error {
owners := []metav1.OwnerReference{{
APIVersion: v1alpha2.SchemeGroupVersion.String(),
Kind: v1alpha2.ApplicationKind,
@@ -73,27 +74,62 @@ func (ret *reter) apply(ctx context.Context, ac *v1alpha2.ApplicationConfigurati
return ret.Sync(ctx, ac, comps)
}
-func (ret *reter) healthCheck(appfile *appfile.Appfile) error {
+func (ret *appHandler) statusAggregate(appfile *appfile.Appfile) ([]v1alpha2.ApplicationComponentStatus, bool, error) {
+ var appStatus []v1alpha2.ApplicationComponentStatus
+ var healthy = true
for _, wl := range appfile.Workloads {
- pCtx := process.NewContext(wl.Name)
+ var status = v1alpha2.ApplicationComponentStatus{
+ Name: wl.Name,
+ Healthy: true,
+ }
+ pCtx := process.NewContext(wl.Name, appfile.Name)
if err := wl.EvalContext(pCtx); err != nil {
- return err
+ return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, evaluate context error", appfile.Name, wl.Name)
}
for _, tr := range wl.Traits {
if err := tr.EvalContext(pCtx); err != nil {
- return err
+ return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, evaluate context error", appfile.Name, wl.Name, tr.Name)
}
}
- if err := wl.EvalHealth(pCtx, ret.c, appfile.Name); err != nil {
- return err
+
+ workloadHealth, err := wl.EvalHealth(pCtx, ret.r, ret.app.Namespace)
+ if err != nil {
+ return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, check health error", appfile.Name, wl.Name)
}
+ if !workloadHealth {
+ // TODO(wonderflow): we should add a custom way to let the template say why it's unhealthy, only a bool flag is not enough
+ status.Healthy = false
+ healthy = false
+ }
+ status.Message, err = wl.EvalStatus(pCtx, ret.r, ret.app.Namespace)
+ if err != nil {
+ return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, evaluate workload status message error", appfile.Name, wl.Name)
+ }
+ var traitStatusList []v1alpha2.ApplicationTraitStatus
for _, trait := range wl.Traits {
- if err := trait.EvalHealth(pCtx, ret.c, appfile.Name); err != nil {
- return err
+ var traitStatus = v1alpha2.ApplicationTraitStatus{
+ Type: trait.Name,
+ Healthy: true,
}
+ traitHealth, err := trait.EvalHealth(pCtx, ret.r, ret.app.Namespace)
+ if err != nil {
+ return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, check health error", appfile.Name, wl.Name, trait.Name)
+ }
+ if !traitHealth {
+ // TODO(wonderflow): we should add a custom way to let the template say why it's unhealthy, only a bool flag is not enough
+ traitStatus.Healthy = false
+ healthy = false
+ }
+ traitStatus.Message, err = trait.EvalStatus(pCtx, ret.r, ret.app.Namespace)
+ if err != nil {
+ return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, evaluate status message error", appfile.Name, wl.Name, trait.Name)
+ }
+ traitStatusList = append(traitStatusList, traitStatus)
}
+ status.Traits = traitStatusList
+ appStatus = append(appStatus, status)
}
- return nil
+ return appStatus, healthy, nil
}
// CreateOrUpdateComponent will create if not exist and update if exists.
@@ -129,14 +165,14 @@ func CreateOrUpdateAppConfig(ctx context.Context, client client.Client, appConfi
}
// Sync perform synchronization operations
-func (ret *reter) Sync(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, comps []*v1alpha2.Component) error {
+func (ret *appHandler) Sync(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, comps []*v1alpha2.Component) error {
for _, comp := range comps {
- if err := CreateOrUpdateComponent(ctx, ret.c, comp.DeepCopy()); err != nil {
+ if err := CreateOrUpdateComponent(ctx, ret.r, comp.DeepCopy()); err != nil {
return err
}
}
- if err := CreateOrUpdateAppConfig(ctx, ret.c, ac); err != nil {
+ if err := CreateOrUpdateAppConfig(ctx, ret.r, ac); err != nil {
return err
}
@@ -155,7 +191,7 @@ func (ret *reter) Sync(ctx context.Context, ac *v1alpha2.ApplicationConfiguratio
}
// Component not exits in current Application, should be deleted
var oldC = &v1alpha2.Component{ObjectMeta: metav1.ObjectMeta{Name: comp.Name, Namespace: ac.Namespace}}
- if err := ret.c.Delete(ctx, oldC); err != nil {
+ if err := ret.r.Delete(ctx, oldC); err != nil {
return err
}
}
diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go
index dde711750..3fce396c3 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go
@@ -22,11 +22,11 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
- "k8s.io/utils/pointer"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
+ "k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
diff --git a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/applicationconfiguration.go b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/applicationconfiguration.go
index 14a9e9e2f..3b2d588eb 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/applicationconfiguration.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/applicationconfiguration.go
@@ -27,6 +27,8 @@ import (
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/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -102,22 +104,22 @@ func Setup(mgr ctrl.Manager, args core.Args, l logging.Logger) error {
Complete(NewReconciler(mgr, dm,
WithLogger(l.WithValues("controller", name)),
WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))),
- WithApplyOnceOnly(args.ApplyOnceOnly)))
+ WithApplyOnceOnlyMode(args.ApplyMode)))
}
// An OAMApplicationReconciler reconciles OAM ApplicationConfigurations by rendering and
// instantiating their Components and Traits.
type OAMApplicationReconciler struct {
- client client.Client
- components ComponentRenderer
- workloads WorkloadApplicator
- gc GarbageCollector
- scheme *runtime.Scheme
- log logging.Logger
- record event.Recorder
- preHooks map[string]ControllerHooks
- postHooks map[string]ControllerHooks
- applyOnceOnly bool
+ client client.Client
+ components ComponentRenderer
+ workloads WorkloadApplicator
+ gc GarbageCollector
+ scheme *runtime.Scheme
+ log logging.Logger
+ record event.Recorder
+ preHooks map[string]ControllerHooks
+ postHooks map[string]ControllerHooks
+ applyOnceOnlyMode core.ApplyOnceOnlyMode
}
// A ReconcilerOption configures a Reconciler.
@@ -174,11 +176,11 @@ func WithPosthook(name string, hook ControllerHooks) ReconcilerOption {
}
}
-// WithApplyOnceOnly indicates whether workloads and traits should be
+// WithApplyOnceOnlyMode indicates whether workloads and traits should be
// affected if no spec change is made in the ApplicationConfiguration.
-func WithApplyOnceOnly(applyOnceOnly bool) ReconcilerOption {
+func WithApplyOnceOnlyMode(mode core.ApplyOnceOnlyMode) ReconcilerOption {
return func(r *OAMApplicationReconciler) {
- r.applyOnceOnly = applyOnceOnly
+ r.applyOnceOnlyMode = mode
}
}
@@ -200,11 +202,12 @@ func NewReconciler(m ctrl.Manager, dm discoverymapper.DiscoveryMapper, o ...Reco
rawClient: m.GetClient(),
dm: dm,
},
- gc: GarbageCollectorFn(eligible),
- log: logging.NewNopLogger(),
- record: event.NewNopRecorder(),
- preHooks: make(map[string]ControllerHooks),
- postHooks: make(map[string]ControllerHooks),
+ gc: GarbageCollectorFn(eligible),
+ log: logging.NewNopLogger(),
+ record: event.NewNopRecorder(),
+ preHooks: make(map[string]ControllerHooks),
+ postHooks: make(map[string]ControllerHooks),
+ applyOnceOnlyMode: core.ApplyOnceOnlyOff,
}
for _, ro := range o {
@@ -244,7 +247,7 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
"error", err, "requeue-after", result.RequeueAfter)
r.record.Event(ac, event.Warning(reasonCannotFinalizeWorkloads, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errFinalizeWorkloads)))
- return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, ac), errUpdateAppConfigStatus)
+ return reconcile.Result{}, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
}
return reconcile.Result{}, errors.Wrap(r.client.Update(ctx, ac), errUpdateAppConfigStatus)
}
@@ -259,12 +262,12 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
r.record.Event(ac, event.Warning(reasonCannotExecutePosthooks, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errExecutePosthooks)))
result = exeResult
- returnErr = errors.Wrap(r.client.Status().Update(ctx, ac), errUpdateAppConfigStatus)
+ returnErr = errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
return
}
r.record.Event(ac, event.Normal(reasonExecutePosthook, "Successfully executed a posthook", "posthook name", name))
}
- returnErr = errors.Wrap(r.client.Status().Update(ctx, ac), errUpdateAppConfigStatus)
+ 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 {
@@ -279,7 +282,7 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
log.Debug("Failed to execute pre-hooks", "hook name", name, "error", err, "requeue-after", result.RequeueAfter)
r.record.Event(ac, event.Warning(reasonCannotExecutePrehooks, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errExecutePrehooks)))
- return result, errors.Wrap(r.client.Status().Update(ctx, ac), errUpdateAppConfigStatus)
+ return result, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
}
r.record.Event(ac, event.Normal(reasonExecutePrehook, "Successfully executed a prehook", "prehook name ", name))
}
@@ -291,20 +294,17 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
log.Info("Cannot render components", "error", err, "requeue-after", time.Now().Add(shortWait))
r.record.Event(ac, event.Warning(reasonCannotRenderComponents, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errRenderComponents)))
- return errResult, errors.Wrap(r.client.Status().Update(ctx, ac), errUpdateAppConfigStatus)
+ return errResult, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
}
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())}
- if r.applyOnceOnly {
- applyOpts = append(applyOpts, applyOnceOnly())
- }
+ applyOpts := []apply.ApplyOption{apply.MustBeControllableBy(ac.GetUID()), applyOnceOnly(ac, r.applyOnceOnlyMode)}
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))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errApplyComponents)))
- return errResult, errors.Wrap(r.client.Status().Update(ctx, ac), errUpdateAppConfigStatus)
+ return errResult, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
}
log.Debug("Successfully applied components", "workloads", len(workloads))
r.record.Event(ac, event.Normal(reasonApplyComponents, "Successfully applied components", "workloads", strconv.Itoa(len(workloads))))
@@ -324,7 +324,7 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
log.Debug("Cannot garbage collect component", "error", err, "requeue-after", time.Now().Add(shortWait))
record.Event(ac, event.Warning(reasonCannotGGComponents, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errGCComponent)))
- return errResult, errors.Wrap(r.client.Status().Update(ctx, ac), errUpdateAppConfigStatus)
+ return errResult, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
}
log.Debug("Garbage collected resource")
record.Event(ac, event.Normal(reasonGGComponent, "Successfully garbage collected component"))
@@ -344,11 +344,24 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
return reconcile.Result{RequeueAfter: waitTime}, 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
+ return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
+ if err = r.client.Get(ctx, types.NamespacedName{Namespace: ac.Namespace, Name: ac.Name}, ac); err != nil {
+ return
+ }
+ ac.Status = status
+ return r.client.Status().Update(ctx, ac, opts...)
+ })
+}
+
func (r *OAMApplicationReconciler) updateStatus(ctx context.Context, ac, acPatch *v1alpha2.ApplicationConfiguration, workloads []Workload) {
ac.Status.Workloads = make([]v1alpha2.WorkloadStatus, len(workloads))
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
}
@@ -576,36 +589,87 @@ func (e *GenerationUnchanged) Error() string {
"Please ignore this error in other logic.")
}
-func applyOnceOnly() apply.ApplyOption {
- return func(ctx context.Context, current, desired runtime.Object) error {
- if current == nil {
+// 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 {
+ return func(_ context.Context, existing, desired runtime.Object) error {
+ if mode == core.ApplyOnceOnlyOff {
return nil
}
- // ApplyOption only works for update/patch operation and will be ignored
- // if the object doesn't exist before.
- c, _ := current.(metav1.Object)
+
d, _ := desired.(metav1.Object)
- if c == nil || d == nil {
- return errors.Errorf("invalid object being applied: %q ",
+ if d == nil {
+ return errors.Errorf("cannot access metadata of object being applied: %q",
desired.GetObjectKind().GroupVersionKind())
}
- cLabels, dLabels := c.GetLabels(), d.GetLabels()
- if dLabels[oam.LabelOAMResourceType] == oam.ResourceTypeWorkload ||
- dLabels[oam.LabelOAMResourceType] == oam.ResourceTypeTrait {
- // check whether spec changes occur on the workload or trait,
- // according to annotations and lables
- if c.GetAnnotations()[oam.AnnotationAppGeneration] !=
- d.GetAnnotations()[oam.AnnotationAppGeneration] {
- return nil
- }
- if cLabels[oam.LabelAppComponentRevision] != dLabels[oam.LabelAppComponentRevision] ||
- cLabels[oam.LabelAppComponent] != dLabels[oam.LabelAppComponent] ||
- cLabels[oam.LabelAppName] != dLabels[oam.LabelAppName] {
- return nil
- }
- // return an error to abort current apply
- return &GenerationUnchanged{}
+ dLabels := d.GetLabels()
+ dAnnots := d.GetAnnotations()
+ if dLabels[oam.LabelOAMResourceType] != oam.ResourceTypeWorkload &&
+ 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
+ return nil
}
- return nil
+
+ // the resource doesn't exist (maybe not created before, or created but deleted by others)
+ if existing == nil {
+ if mode != core.ApplyOnceOnlyForce {
+ // non-force mode will always create the resource if not exist.
+ return nil
+ }
+
+ createdBefore := false
+ 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
+ }
+ 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
+ }
+ }
+ }
+ // 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{}
+ }
+ }
+ // no recorded workloads nor traits matches the applied resource
+ // that means the resource is not created before, so create it
+ return nil
+ }
+
+ // the resource already exists
+ e, _ := existing.(metav1.Object)
+ if e == nil {
+ return errors.Errorf("cannot access metadata of existing object: %q",
+ existing.GetObjectKind().GroupVersionKind())
+ }
+ eLabels := e.GetLabels()
+ // if existing reource'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]) {
+ // its spec is changed, so apply new configuration to it
+ return nil
+ }
+ // its spec is not changed, return an error to abort applying it
+ return &GenerationUnchanged{}
}
}
diff --git a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/apply_once_only_test.go b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/apply_once_only_test.go
index e1ce7cbea..05d05147f 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/apply_once_only_test.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/apply_once_only_test.go
@@ -16,6 +16,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
+ core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
@@ -31,21 +32,25 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
traitSpecValue2 = "test2"
)
var (
- ctx = context.Background()
- cw v1alpha2.ContainerizedWorkload
- component v1alpha2.Component
- fakeTrait *unstructured.Unstructured
- appConfig v1alpha2.ApplicationConfiguration
+ ctx = context.Background()
+ cw v1alpha2.ContainerizedWorkload
+ component v1alpha2.Component
+ fakeTrait *unstructured.Unstructured
+ appConfig v1alpha2.ApplicationConfiguration
+ cwObjKey = client.ObjectKey{
+ Name: compName,
+ Namespace: namespace,
+ }
+ traitObjKey = client.ObjectKey{
+ Name: traitName,
+ Namespace: namespace,
+ }
appConfigKey = client.ObjectKey{
Name: appName,
Namespace: namespace,
}
req = reconcile.Request{NamespacedName: appConfigKey}
- ns = corev1.Namespace{
- ObjectMeta: metav1.ObjectMeta{
- Name: namespace,
- },
- }
+ ns corev1.Namespace
)
BeforeEach(func() {
@@ -112,19 +117,12 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
},
}
- logf.Log.Info("Start to run a test, clean up previous resources")
- // delete the namespace with all its resources
- Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).
- Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{}))
- logf.Log.Info("make sure all the resources are removed")
- Eventually(
- // gomega has a bug that can't take nil as the actual input, so has to make it a func
- func() error {
- return k8sClient.Get(ctx, client.ObjectKey{Name: namespace}, &corev1.Namespace{})
- },
- time.Second*120, time.Millisecond*500).Should(&util.NotFoundMatcher{})
-
By("Create namespace")
+ ns = corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: namespace,
+ },
+ }
Eventually(
func() error {
return k8sClient.Create(ctx, &ns)
@@ -142,24 +140,33 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
return k8sClient.Get(ctx, appConfigKey, &appConfig)
}, time.Second, 300*time.Millisecond).Should(BeNil())
- By("Enable ApplyOnceOnly")
- reconciler.applyOnceOnly = true
-
By("Reconcile")
Expect(func() error { _, err := reconciler.Reconcile(req); return err }()).Should(BeNil())
+ time.Sleep(3 * time.Second)
})
AfterEach(func() {
+ logf.Log.Info("Clean up previous resources")
+ Expect(k8sClient.DeleteAllOf(ctx, &appConfig, client.InNamespace(namespace))).Should(Succeed())
+ Expect(k8sClient.DeleteAllOf(ctx, &cw, client.InNamespace(namespace))).Should(Succeed())
+ Expect(k8sClient.DeleteAllOf(ctx, &component, client.InNamespace(namespace))).Should(Succeed())
+ var deleteTrait unstructured.Unstructured
+ deleteTrait.SetAPIVersion("example.com/v1")
+ deleteTrait.SetKind("Foo")
+ Expect(k8sClient.DeleteAllOf(ctx, &deleteTrait, client.InNamespace(namespace))).Should(Succeed())
// restore as default value
- reconciler.applyOnceOnly = false
+ reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOff
})
- When("Change workload/trait instance bypass ApplicationConfiguration", func() {
- It("should keep workload instanced not changed by reconciliation", func() {
+ When("ApplyOnceOnly is enabled", func() {
+ It("should not revert changes of workload/trait made by others", func() {
+ By("Enable ApplyOnceOnly")
+ reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOn
+
By("Get workload instance & Check workload spec")
cwObj := v1alpha2.ContainerizedWorkload{}
Eventually(func() error {
- return k8sClient.Get(ctx, client.ObjectKey{Name: compName, Namespace: namespace}, &cwObj)
+ return k8sClient.Get(ctx, cwObjKey, &cwObj)
}, 5*time.Second, time.Second).Should(BeNil())
Expect(cwObj.Spec.Containers[0].Image).Should(Equal(image1))
@@ -168,7 +175,7 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
fooObj.SetAPIVersion("example.com/v1")
fooObj.SetKind("Foo")
Eventually(func() error {
- return k8sClient.Get(ctx, client.ObjectKey{Name: traitName, Namespace: namespace}, fooObj)
+ return k8sClient.Get(ctx, traitObjKey, fooObj)
}, 3*time.Second, time.Second).Should(BeNil())
fooObjV, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
Expect(fooObjV).Should(Equal(traitSpecValue1))
@@ -184,7 +191,7 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
By("Get updated workload instance & Check workload spec")
updateCwObj := v1alpha2.ContainerizedWorkload{}
Eventually(func() string {
- if err := k8sClient.Get(ctx, client.ObjectKey{Name: compName, Namespace: namespace}, &updateCwObj); err != nil {
+ if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
return ""
}
return updateCwObj.Spec.Containers[0].Image
@@ -195,7 +202,7 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
updatedFooObj.SetAPIVersion("example.com/v1")
updatedFooObj.SetKind("Foo")
Eventually(func() string {
- if err := k8sClient.Get(ctx, client.ObjectKey{Name: traitName, Namespace: namespace}, updatedFooObj); err != nil {
+ if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
return ""
}
v, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
@@ -209,7 +216,7 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
By("Check workload is not changed by reconciliation")
updateCwObj = v1alpha2.ContainerizedWorkload{}
Eventually(func() string {
- if err := k8sClient.Get(ctx, client.ObjectKey{Name: compName, Namespace: namespace}, &updateCwObj); err != nil {
+ if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
return ""
}
return updateCwObj.Spec.Containers[0].Image
@@ -220,7 +227,7 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
updatedFooObj.SetAPIVersion("example.com/v1")
updatedFooObj.SetKind("Foo")
Eventually(func() string {
- if err := k8sClient.Get(ctx, client.ObjectKey{Name: traitName, Namespace: namespace}, updatedFooObj); err != nil {
+ if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
return ""
}
v, _, _ := unstructured.NestedString(updatedFooObj.Object, "spec", "key")
@@ -228,13 +235,13 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
}, 3*time.Second, time.Second).Should(Equal(traitSpecValue2))
By("Disable ApplyOnceOnly & Reconcile again")
- reconciler.applyOnceOnly = false
+ reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOff
Expect(func() error { _, err := reconciler.Reconcile(req); return err }()).Should(BeNil())
By("Check workload is changed by reconciliation")
updateCwObj = v1alpha2.ContainerizedWorkload{}
Eventually(func() string {
- if err := k8sClient.Get(ctx, client.ObjectKey{Name: compName, Namespace: namespace}, &updateCwObj); err != nil {
+ if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
return ""
}
return updateCwObj.Spec.Containers[0].Image
@@ -245,13 +252,213 @@ var _ = Describe("Test apply (workloads/traits) once only", func() {
updatedFooObj.SetAPIVersion("example.com/v1")
updatedFooObj.SetKind("Foo")
Eventually(func() string {
- if err := k8sClient.Get(ctx, client.ObjectKey{Name: traitName, Namespace: namespace}, updatedFooObj); err != nil {
+ if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
return ""
}
v, _, _ := unstructured.NestedString(updatedFooObj.Object, "spec", "key")
return v
}, 3*time.Second, time.Second).Should(Equal(traitSpecValue1))
})
+
+ It("should re-create workload/trait if it's delete by others", func() {
+ By("Enable ApplyOnceOnly")
+ reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOn
+
+ By("Get workload instance & Check workload spec")
+ cwObj := v1alpha2.ContainerizedWorkload{}
+ Eventually(func() error {
+ return k8sClient.Get(ctx, cwObjKey, &cwObj)
+ }, 5*time.Second, time.Second).Should(BeNil())
+
+ By("Delete the workload")
+ Expect(k8sClient.Delete(ctx, &cwObj)).Should(Succeed())
+ Expect(k8sClient.Get(ctx, cwObjKey, &v1alpha2.ContainerizedWorkload{})).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 created by reconciliation")
+ recreatedCwObj := v1alpha2.ContainerizedWorkload{}
+ Expect(k8sClient.Get(ctx, cwObjKey, &recreatedCwObj)).Should(Succeed())
+ })
})
+ When("ApplyOnceOnlyForce is enabled", func() {
+ It("should not revert changes of workload/trait made by others", func() {
+ By("Enable ApplyOnceOnlyForce")
+ reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
+
+ By("Get workload instance & Check workload spec")
+ cwObj := v1alpha2.ContainerizedWorkload{}
+ Eventually(func() error {
+ return k8sClient.Get(ctx, cwObjKey, &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, traitObjKey, fooObj)
+ }, 3*time.Second, time.Second).Should(BeNil())
+ fooObjV, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
+ Expect(fooObjV).Should(Equal(traitSpecValue1))
+
+ By("Modify workload spec & Apply changed workload")
+ cwObj.Spec.Containers[0].Image = image2
+ Expect(k8sClient.Patch(ctx, &cwObj, client.Merge)).Should(Succeed())
+
+ By("Modify trait spec & Apply changed trait")
+ unstructured.SetNestedField(fooObj.Object, traitSpecValue2, "spec", "key")
+ Expect(k8sClient.Patch(ctx, fooObj, client.Merge)).Should(Succeed())
+
+ By("Get updated workload instance & Check workload spec")
+ updateCwObj := v1alpha2.ContainerizedWorkload{}
+ Eventually(func() string {
+ if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
+ return ""
+ }
+ return updateCwObj.Spec.Containers[0].Image
+ }, 3*time.Second, time.Second).Should(Equal(image2))
+
+ By("Get updated trait instance & Check trait spec")
+ updatedFooObj := &unstructured.Unstructured{}
+ updatedFooObj.SetAPIVersion("example.com/v1")
+ updatedFooObj.SetKind("Foo")
+ Eventually(func() string {
+ if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
+ return ""
+ }
+ v, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
+ return v
+ }, 3*time.Second, time.Second).Should(Equal(traitSpecValue2))
+
+ By("Reconcile")
+ Expect(func() error { _, err := reconciler.Reconcile(req); return err }()).Should(BeNil())
+ time.Sleep(3 * time.Second)
+
+ By("Check workload is not changed by reconciliation")
+ updateCwObj = v1alpha2.ContainerizedWorkload{}
+ Eventually(func() string {
+ if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
+ return ""
+ }
+ return updateCwObj.Spec.Containers[0].Image
+ }, 3*time.Second, time.Second).Should(Equal(image2))
+
+ By("Check trait is not changed by reconciliation")
+ updatedFooObj = &unstructured.Unstructured{}
+ updatedFooObj.SetAPIVersion("example.com/v1")
+ updatedFooObj.SetKind("Foo")
+ Eventually(func() string {
+ if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
+ return ""
+ }
+ v, _, _ := unstructured.NestedString(updatedFooObj.Object, "spec", "key")
+ return v
+ }, 3*time.Second, time.Second).Should(Equal(traitSpecValue2))
+
+ By("Disable ApplyOnceOnly & Reconcile again")
+ reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOff
+ Expect(func() error { _, err := reconciler.Reconcile(req); return err }()).Should(BeNil())
+
+ By("Check workload is changed by reconciliation")
+ updateCwObj = v1alpha2.ContainerizedWorkload{}
+ Eventually(func() string {
+ if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
+ return ""
+ }
+ return updateCwObj.Spec.Containers[0].Image
+ }, 3*time.Second, time.Second).Should(Equal(image1))
+
+ By("Check trait is changed by reconciliation")
+ updatedFooObj = &unstructured.Unstructured{}
+ updatedFooObj.SetAPIVersion("example.com/v1")
+ updatedFooObj.SetKind("Foo")
+ Eventually(func() string {
+ if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
+ return ""
+ }
+ v, _, _ := unstructured.NestedString(updatedFooObj.Object, "spec", "key")
+ return v
+ }, 3*time.Second, time.Second).Should(Equal(traitSpecValue1))
+ })
+
+ It("should not re-create workload/trait if it's delete by others", func() {
+ By("Enable ApplyOnceOnlyForce")
+ reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
+
+ By("Get workload instance")
+ cwObj := v1alpha2.ContainerizedWorkload{}
+ Eventually(func() error {
+ return k8sClient.Get(ctx, cwObjKey, &cwObj)
+ }, 3*time.Second, time.Second).Should(BeNil())
+
+ 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, traitObjKey, &fooObj)
+ }, 3*time.Second, time.Second).Should(BeNil())
+
+ By("Delete the workload")
+ Expect(k8sClient.Delete(ctx, &cwObj)).Should(Succeed())
+ Expect(k8sClient.Get(ctx, cwObjKey, &v1alpha2.ContainerizedWorkload{})).Should(util.NotFoundMatcher{})
+
+ By("Delete the trait")
+ Expect(k8sClient.Delete(ctx, &fooObj)).Should(Succeed())
+ Expect(k8sClient.Get(ctx, traitObjKey, &fooObj)).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")
+ recreatedCwObj := v1alpha2.ContainerizedWorkload{}
+ Expect(k8sClient.Get(ctx, cwObjKey, &recreatedCwObj)).Should(util.NotFoundMatcher{})
+
+ By("Check trait is not re-created by reconciliation")
+ recreatedFooObj := unstructured.Unstructured{}
+ recreatedFooObj.SetAPIVersion("example.com/v1")
+ recreatedFooObj.SetKind("Foo")
+ Expect(k8sClient.Get(ctx, traitObjKey, &recreatedFooObj)).Should(util.NotFoundMatcher{})
+
+ By("Update Appconfig to trigger generation augment")
+ unstructured.SetNestedField(fakeTrait.Object, "newvalue", "spec", "key")
+ appConfig = v1alpha2.ApplicationConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: appName,
+ Namespace: namespace,
+ },
+ Spec: v1alpha2.ApplicationConfigurationSpec{
+ Components: []v1alpha2.ApplicationConfigurationComponent{
+ {
+ ComponentName: compName,
+ Traits: []v1alpha2.ComponentTrait{
+ {Trait: runtime.RawExtension{Object: fakeTrait}},
+ },
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Patch(ctx, &appConfig, client.Merge)).Should(Succeed())
+
+ By("Reconcile")
+ reconcileRetry(reconciler, req)
+ time.Sleep(3 * time.Second)
+
+ By("Check workload is re-created by reconciliation")
+ recreatedCwObj = v1alpha2.ContainerizedWorkload{}
+ Expect(k8sClient.Get(ctx, cwObjKey, &recreatedCwObj)).Should(Succeed())
+
+ By("Check trait is re-created by reconciliation")
+ recreatedFooObj = unstructured.Unstructured{}
+ recreatedFooObj.SetAPIVersion("example.com/v1")
+ recreatedFooObj.SetKind("Foo")
+ Expect(k8sClient.Get(ctx, traitObjKey, &recreatedFooObj)).Should(Succeed())
+ })
+ })
})
diff --git a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/component.go b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/component.go
index a9a086def..a05a49453 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/component.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/component.go
@@ -11,6 +11,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
@@ -191,7 +192,7 @@ func (c *ComponentHandler) createControllerRevision(mt metav1.Object, obj runtim
return nil, false
}
- err = c.Client.Status().Update(context.Background(), comp)
+ err = c.UpdateStatus(context.Background(), comp)
if err != nil {
c.Logger.Info(fmt.Sprintf("update component status latestRevision %s err %v", revisionName, err), "componentName", mt.GetName())
return nil, false
@@ -277,6 +278,18 @@ func (c *ComponentHandler) cleanupControllerRevision(curComp *v1alpha2.Component
return nil
}
+// UpdateStatus updates v1alpha2.Component's Status with retry.RetryOnConflict
+func (c *ComponentHandler) UpdateStatus(ctx context.Context, comp *v1alpha2.Component, opts ...client.UpdateOption) error {
+ status := comp.DeepCopy().Status
+ return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
+ if err = c.Client.Get(ctx, types.NamespacedName{Namespace: comp.Namespace, Name: comp.Name}, comp); err != nil {
+ return
+ }
+ comp.Status = status
+ return c.Client.Status().Update(ctx, comp, opts...)
+ })
+}
+
// ConstructRevisionName will generate revisionName from componentName
// will be -v, for example: comp-v1
func ConstructRevisionName(componentName string, revision int64) string {
diff --git a/pkg/controller/core.oam.dev/v1alpha2/applicationdeployment/applicationdeployment_controller.go b/pkg/controller/core.oam.dev/v1alpha2/applicationdeployment/applicationdeployment_controller.go
index 898e34e82..68b4efd9d 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/applicationdeployment/applicationdeployment_controller.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/applicationdeployment/applicationdeployment_controller.go
@@ -14,6 +14,7 @@ import (
"k8s.io/kubectl/pkg/util/slice"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
corev1alpha2 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/controller/common/rollout"
@@ -24,7 +25,7 @@ import (
)
const appDeployFinalizer = "finalizers.applicationdeployment.oam.dev"
-const reconcileTimeOut = 10 * time.Second
+const reconcileTimeOut = 30 * time.Second
// Reconciler reconciles an ApplicationDeployment object
type Reconciler struct {
@@ -40,10 +41,25 @@ type Reconciler struct {
// +kubebuilder:rbac:groups=core.oam.dev,resources=applications/status,verbs=get;update;patch
// Reconcile is the main logic of applicationdeployment controller
-func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
+func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr error) {
var appDeploy corev1alpha2.ApplicationDeployment
ctx, cancel := context.WithTimeout(context.TODO(), reconcileTimeOut)
defer cancel()
+
+ startTime := time.Now()
+ defer func() {
+ if retErr == nil {
+ if res.Requeue || res.RequeueAfter > 0 {
+ klog.InfoS("Finished reconciling appDeployment", "deployment", req, "time spent",
+ time.Since(startTime), "result", res)
+ } else {
+ klog.InfoS("Finished reconcile appDeployment", "deployment", req, "time spent", time.Since(startTime))
+ }
+ } else {
+ klog.Errorf("Failed to reconcile appDeployment %s: %v", req, retErr)
+ }
+ }()
+
if err := r.Get(ctx, req.NamespacedName, &appDeploy); err != nil {
if apierrors.IsNotFound(err) {
klog.InfoS("application deployment does not exist", "appDeploy", klog.KRef(req.Namespace, req.Name))
@@ -52,6 +68,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
klog.InfoS("Start to reconcile ", "application deployment", klog.KObj(&appDeploy))
+ // TODO: check if the target/source has changed
r.handleFinalizer(&appDeploy)
// Get the target application
@@ -88,7 +105,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
klog.ErrorS(err, "cannot fetch the workloads to upgrade", "workload Type", workloadType,
"workload GVK", *workloadGVK, "target application", klog.KRef(req.Namespace, targetAppName),
"source application", klog.KRef(req.Namespace, sourceAppName))
- return ctrl.Result{}, client.IgnoreNotFound(err)
+ return ctrl.Result{RequeueAfter: 5 * time.Second}, client.IgnoreNotFound(err)
}
klog.InfoS("get the target workload we need to work on", "targetWorkload", klog.KObj(targetWorkload))
@@ -108,13 +125,13 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
// reconcile the rollout part of the spec given the target and source workload
- err = rollout.ReconcileRolloutPlan(ctx, r, &appDeploy.Spec.RolloutPlan, targetWorkload, sourceWorkload)
- if err != nil {
- klog.ErrorS(err, "cannot reconcile the rollout plan", "rollout spec", appDeploy.Spec.RolloutPlan)
- return ctrl.Result{}, err
- }
-
- return ctrl.Result{}, nil
+ rolloutPlanController := rollout.NewRolloutPlanController(r, &appDeploy, r.record,
+ &appDeploy.Spec.RolloutPlan, appDeploy.Status.RolloutStatus, targetWorkload, sourceWorkload)
+ result, rolloutStatus := rolloutPlanController.Reconcile(ctx)
+ // make sure that the new status is copied back
+ appDeploy.Status.RolloutStatus = rolloutStatus
+ // update the appDeploy status
+ return result, r.Update(ctx, &appDeploy)
}
func (r *Reconciler) handleFinalizer(appDeploy *corev1alpha2.ApplicationDeployment) {
diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope_controller.go b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope_controller.go
index a63efe935..a6615db6a 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope_controller.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope_controller.go
@@ -23,6 +23,8 @@ import (
"time"
"github.com/pkg/errors"
+ "k8s.io/apimachinery/pkg/types"
+ "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/reconcile"
@@ -172,7 +174,7 @@ func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error)
hs.Status.ScopeHealthCondition = scopeCondition
hs.Status.WorkloadHealthConditions = wlConditions
- return reconcile.Result{RequeueAfter: interval - elapsed}, errors.Wrap(r.client.Status().Update(ctx, hs), errUpdateHealthScopeStatus)
+ return reconcile.Result{RequeueAfter: interval - elapsed}, errors.Wrap(r.UpdateStatus(ctx, hs), errUpdateHealthScopeStatus)
}
// GetScopeHealthStatus get the status of the healthscope based on workload resources.
@@ -257,3 +259,15 @@ func (r *Reconciler) GetScopeHealthStatus(ctx context.Context, healthScope *v1al
return scopeCondition, workloadHealthConditions
}
+
+// UpdateStatus updates v1alpha2.HealthScope's Status with retry.RetryOnConflict
+func (r *Reconciler) UpdateStatus(ctx context.Context, hs *v1alpha2.HealthScope, opts ...client.UpdateOption) error {
+ status := hs.DeepCopy().Status
+ return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
+ if err = r.client.Get(ctx, types.NamespacedName{Namespace: hs.Namespace, Name: hs.Name}, hs); err != nil {
+ return
+ }
+ hs.Status = status
+ return r.client.Status().Update(ctx, hs, opts...)
+ })
+}
diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/workloads/containerizedworkload/containerizedworkload_controller.go b/pkg/controller/core.oam.dev/v1alpha2/core/workloads/containerizedworkload/containerizedworkload_controller.go
index e83a484f1..0b382b283 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/core/workloads/containerizedworkload/containerizedworkload_controller.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/core/workloads/containerizedworkload/containerizedworkload_controller.go
@@ -29,6 +29,8 @@ import (
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -172,12 +174,24 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
},
)
- if err := r.Status().Update(ctx, &workload); err != nil {
+ if err := r.UpdateStatus(ctx, &workload); err != nil {
return util.ReconcileWaitResult, err
}
return ctrl.Result{}, util.PatchCondition(ctx, r, &workload, cpv1alpha1.ReconcileSuccess())
}
+// UpdateStatus updates v1alpha2.ContainerizedWorkload's Status with retry.RetryOnConflict
+func (r *Reconciler) UpdateStatus(ctx context.Context, workload *v1alpha2.ContainerizedWorkload, opts ...client.UpdateOption) error {
+ status := workload.DeepCopy().Status
+ return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
+ if err = r.Get(ctx, types.NamespacedName{Namespace: workload.Namespace, Name: workload.Name}, workload); err != nil {
+ return
+ }
+ workload.Status = status
+ return r.Status().Update(ctx, workload, opts...)
+ })
+}
+
// SetupWithManager setups up k8s controller.
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
src := &v1alpha2.ContainerizedWorkload{}
diff --git a/pkg/controller/standard.oam.dev/v1alpha1/metrics/metricstrait_controller.go b/pkg/controller/standard.oam.dev/v1alpha1/metrics/metricstrait_controller.go
index c7e20d183..2779a3e1b 100644
--- a/pkg/controller/standard.oam.dev/v1alpha1/metrics/metricstrait_controller.go
+++ b/pkg/controller/standard.oam.dev/v1alpha1/metrics/metricstrait_controller.go
@@ -31,7 +31,9 @@ import (
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"
@@ -163,7 +165,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
r.gcOrphanServiceMonitor(ctx, mLog, &metricsTrait)
(&metricsTrait).SetConditions(cpv1alpha1.ReconcileSuccess())
- return ctrl.Result{}, errors.Wrap(r.Status().Update(ctx, &metricsTrait), common.ErrUpdateStatus)
+ return ctrl.Result{}, errors.Wrap(r.UpdateStatus(ctx, &metricsTrait), common.ErrUpdateStatus)
}
// fetch the label of the service that is associated with the workload
@@ -336,6 +338,18 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
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())
diff --git a/pkg/controller/standard.oam.dev/v1alpha1/podspecworkload/podspecworkload_controller.go b/pkg/controller/standard.oam.dev/v1alpha1/podspecworkload/podspecworkload_controller.go
index 5a0b83b0e..6d71459e5 100644
--- a/pkg/controller/standard.oam.dev/v1alpha1/podspecworkload/podspecworkload_controller.go
+++ b/pkg/controller/standard.oam.dev/v1alpha1/podspecworkload/podspecworkload_controller.go
@@ -30,7 +30,9 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
+ "k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -151,7 +153,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
})
}
- if err := r.Status().Update(ctx, &workload); err != nil {
+ if err := r.UpdateStatus(ctx, &workload); err != nil {
return util.ReconcileWaitResult, err
}
return ctrl.Result{}, util.PatchCondition(ctx, r, &workload, cpv1alpha1.ReconcileSuccess())
@@ -277,6 +279,18 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
Complete(r)
}
+// UpdateStatus updates *v1alpha1.PodSpecWorkload's Status with retry.RetryOnConflict
+func (r *Reconciler) UpdateStatus(ctx context.Context, workload *v1alpha1.PodSpecWorkload, opts ...client.UpdateOption) error {
+ status := workload.DeepCopy().Status
+ return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
+ if err = r.Get(ctx, types.NamespacedName{Namespace: workload.Namespace, Name: workload.Name}, workload); err != nil {
+ return
+ }
+ workload.Status = status
+ return r.Status().Update(ctx, workload, opts...)
+ })
+}
+
// Setup adds a controller that reconciles PodSpecWorkload.
func Setup(mgr ctrl.Manager) error {
reconciler := Reconciler{
diff --git a/pkg/controller/standard.oam.dev/v1alpha1/routes/ingress/nginx_ingress.go b/pkg/controller/standard.oam.dev/v1alpha1/routes/ingress/nginx_ingress.go
index a6a200ba1..ca3e5bb51 100644
--- a/pkg/controller/standard.oam.dev/v1alpha1/routes/ingress/nginx_ingress.go
+++ b/pkg/controller/standard.oam.dev/v1alpha1/routes/ingress/nginx_ingress.go
@@ -121,7 +121,7 @@ func (*Nginx) Construct(routeTrait *standardv1alpha1.Route) []*v1beta1.Ingress {
var annotations = make(map[string]string)
- annotations["kubernetes.io/ingress.class"] = TypeNginx
+ annotations["kubernetes.io/ingress.class"] = routeTrait.Spec.IngressClass
// SSL
if routeTrait.Spec.TLS != nil {
diff --git a/pkg/controller/standard.oam.dev/v1alpha1/routes/ingress/nginx_ingress_test.go b/pkg/controller/standard.oam.dev/v1alpha1/routes/ingress/nginx_ingress_test.go
index f1670dccd..b1f08368e 100644
--- a/pkg/controller/standard.oam.dev/v1alpha1/routes/ingress/nginx_ingress_test.go
+++ b/pkg/controller/standard.oam.dev/v1alpha1/routes/ingress/nginx_ingress_test.go
@@ -46,6 +46,7 @@ func TestConstruct(t *testing.T) {
},
},
},
+ IngressClass: "nginx-private",
},
},
exp: []*v1beta1.Ingress{
@@ -57,7 +58,7 @@ func TestConstruct(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "trait-test-myrule1",
Annotations: map[string]string{
- "kubernetes.io/ingress.class": "nginx",
+ "kubernetes.io/ingress.class": "nginx-private",
"cert-manager.io/issuer": "test-issuer",
},
OwnerReferences: []metav1.OwnerReference{
diff --git a/pkg/controller/standard.oam.dev/v1alpha1/routes/route_controller.go b/pkg/controller/standard.oam.dev/v1alpha1/routes/route_controller.go
index e91678501..ec343d6e9 100644
--- a/pkg/controller/standard.oam.dev/v1alpha1/routes/route_controller.go
+++ b/pkg/controller/standard.oam.dev/v1alpha1/routes/route_controller.go
@@ -23,11 +23,10 @@ import (
"reflect"
"time"
- "github.com/oam-dev/kubevela/pkg/controller/utils"
-
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"
@@ -39,7 +38,9 @@ import (
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"
@@ -146,9 +147,9 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
routeTrait.Status.Status, conditions = routeIngress.CheckStatus(&routeTrait)
routeTrait.Status.Conditions = conditions
if routeTrait.Status.Status != ingress.StatusReady {
- return ctrl.Result{RequeueAfter: requeueNotReady}, r.Status().Update(ctx, &routeTrait)
+ return ctrl.Result{RequeueAfter: requeueNotReady}, r.UpdateStatus(ctx, &routeTrait)
}
- err = r.Status().Update(ctx, &routeTrait)
+ err = r.UpdateStatus(ctx, &routeTrait)
if err != nil {
return oamutil.ReconcileWaitResult, err
}
@@ -247,6 +248,18 @@ func (r *Reconciler) fillBackendByCreatedService(ctx context.Context, mLog logr.
}, 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) {
diff --git a/pkg/dsl/definition/template.go b/pkg/dsl/definition/template.go
index 4014072de..c9c7e3bb4 100644
--- a/pkg/dsl/definition/template.go
+++ b/pkg/dsl/definition/template.go
@@ -8,16 +8,14 @@ import (
"cuelang.org/go/cue"
"cuelang.org/go/cue/build"
"github.com/pkg/errors"
- kerrors "k8s.io/apimachinery/pkg/api/errors"
- "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/dsl/model"
"github.com/oam-dev/kubevela/pkg/dsl/process"
"github.com/oam-dev/kubevela/pkg/dsl/task"
"github.com/oam-dev/kubevela/pkg/oam"
+ "github.com/oam-dev/kubevela/pkg/oam/util"
)
const (
@@ -27,6 +25,10 @@ const (
OutputsFieldName = "outputs"
// PatchFieldName is the name of the struct contains the patch of CR data
PatchFieldName = "patch"
+ // CustomMessage defines the custom message in definition template
+ CustomMessage = "message"
+ // HealthCheckPolicy defines the health check policy in definition template
+ HealthCheckPolicy = "isHealth"
)
const (
@@ -35,194 +37,257 @@ const (
AuxiliaryWorkload = "AuxiliaryWorkload"
)
-var (
- metadataAccessor = meta.NewAccessor()
-)
-
-// Template defines Definition's Render interface
-type Template interface {
- Params(params interface{}) Template
- Complete(ctx process.Context) error
- Output(ctx process.Context, client client.Client, name string) Template
- HealthCheck() error
+// AbstractEngine defines Definition's Render interface
+type AbstractEngine interface {
+ Params(params interface{}) AbstractEngine
+ Complete(ctx process.Context, abstractTemplate string) error
+ HealthCheck(ctx process.Context, cli client.Client, ns string, healthPolicyTemplate string) (bool, error)
+ Status(ctx process.Context, cli client.Client, ns string, customStatusTemplate string) (string, error)
}
type def struct {
name string
- templ string
- health string
params interface{}
- output map[string]interface{}
}
type workloadDef struct {
def
}
-// NewWDTemplater create Workload Definition templater
-func NewWDTemplater(name, templ, health string) Template {
+// NewWorkloadAbstractEngine create Workload Definition AbstractEngine
+func NewWorkloadAbstractEngine(name string) AbstractEngine {
return &workloadDef{
def: def{
name: name,
- templ: templ,
- health: health,
params: nil,
- output: nil,
},
}
}
// Params set definition's params
-func (wd *workloadDef) Params(params interface{}) Template {
+func (wd *workloadDef) Params(params interface{}) AbstractEngine {
wd.params = params
return wd
}
// Complete do workload definition's rendering
-func (wd *workloadDef) Complete(ctx process.Context) error {
+func (wd *workloadDef) Complete(ctx process.Context, abstractTemplate string) error {
bi := build.NewContext().NewInstance("", nil)
- if err := bi.AddFile("-", wd.templ); err != nil {
- return err
+ if err := bi.AddFile("-", abstractTemplate); err != nil {
+ return errors.WithMessagef(err, "invalid cue template of workload %s", wd.name)
}
if wd.params != nil {
- bt, _ := json.Marshal(wd.params)
+ bt, err := json.Marshal(wd.params)
+ if err != nil {
+ return errors.WithMessagef(err, "marshal parameter of workload %s", wd.name)
+ }
if err := bi.AddFile("parameter", fmt.Sprintf("parameter: %s", string(bt))); err != nil {
- return err
+ return errors.WithMessagef(err, "invalid parameter of workload %s", wd.name)
}
}
- if err := bi.AddFile("-", ctx.Compile("context")); err != nil {
+ if err := bi.AddFile("-", ctx.BaseContextFile()); err != nil {
return err
}
- insts := cue.Build([]*build.Instance{bi})
- for _, inst := range insts {
+ instances := cue.Build([]*build.Instance{bi})
+ for _, inst := range instances {
if err := inst.Value().Err(); err != nil {
- return errors.WithMessagef(err, "workloadDef %s eval", wd.name)
+ return errors.WithMessagef(err, "invalid cue template of workload %s after merge parameter and context", wd.name)
}
output := inst.Lookup(OutputFieldName)
base, err := model.NewBase(output)
if err != nil {
- return errors.WithMessagef(err, "workloadDef %s new base", wd.name)
+ return errors.WithMessagef(err, "invalid output of workload %s", wd.name)
}
ctx.SetBase(base)
// we will support outputs for workload composition, and it will become trait in AppConfig.
outputs := inst.Lookup(OutputsFieldName)
+ if !outputs.Exists() {
+ continue
+ }
st, err := outputs.Struct()
- if err == nil {
- for i := 0; i < st.Len(); i++ {
- fieldInfo := st.Field(i)
- if fieldInfo.IsDefinition || fieldInfo.IsHidden || fieldInfo.IsOptional {
- continue
- }
- other, err := model.NewOther(fieldInfo.Value)
- if err != nil {
- return errors.WithMessagef(err, "parse WorkloadDefinition %s outputs(%s)", wd.name, fieldInfo.Name)
- }
- ctx.PutAssistants(process.Assistant{Ins: other, Type: AuxiliaryWorkload})
+ if err != nil {
+ return errors.WithMessagef(err, "invalid outputs of workload %s", wd.name)
+ }
+ for i := 0; i < st.Len(); i++ {
+ fieldInfo := st.Field(i)
+ if fieldInfo.IsDefinition || fieldInfo.IsHidden || fieldInfo.IsOptional {
+ continue
}
+ other, err := model.NewOther(fieldInfo.Value)
+ if err != nil {
+ return errors.WithMessagef(err, "invalid outputs(%s) of workload %s", fieldInfo.Name, wd.name)
+ }
+ ctx.AppendAuxiliaries(process.Auxiliary{Ins: other, Type: AuxiliaryWorkload, Name: fieldInfo.Name, IsOutputs: true})
}
}
return nil
}
-// Output fetch the workload cr and set result to context
-func (wd *workloadDef) Output(ctx process.Context, client client.Client, name string) Template {
- base, _ := ctx.Output()
+func (wd *workloadDef) getTemplateContext(ctx process.Context, cli client.Reader, ns string) (map[string]interface{}, error) {
+
+ var commonLabels = map[string]string{}
+ var root = map[string]interface{}{}
+ for k, v := range ctx.BaseContextLabels() {
+ root[k] = v
+ switch k {
+ case "appName":
+ commonLabels[oam.LabelAppName] = v
+ case "name":
+ commonLabels[oam.LabelAppComponent] = v
+ }
+ }
+
+ base, assists := ctx.Output()
componentWorkload, err := base.Unstructured()
if err != nil {
- return wd
+ return nil, err
}
- workloadCr, err := getObj(client, componentWorkload, name)
+ // workload main resource will have a unique label("app.oam.dev/resourceType"="WORKLOAD") in per component/app level
+ object, err := getResourceFromObj(componentWorkload, cli, ns, util.MergeMapOverrideWithDst(map[string]string{
+ oam.LabelOAMResourceType: oam.ResourceTypeWorkload,
+ }, commonLabels), "")
if err != nil {
- return wd
+ return nil, err
}
- wd.output = workloadCr
- return wd
+ root[OutputFieldName] = object
+ outputs := make(map[string]interface{})
+ for _, assist := range assists {
+ if assist.Type != AuxiliaryWorkload {
+ continue
+ }
+ if assist.Name == "" {
+ return nil, errors.New("the auxiliary of workload must have a name with format 'outputs.'")
+ }
+ traitRef, err := assist.Ins.Unstructured()
+ if err != nil {
+ return nil, err
+ }
+ // AuxiliaryWorkload will have a unique label("trait.oam.dev/resource"="name of outputs") in per component/app level
+ object, err := getResourceFromObj(traitRef, cli, ns, util.MergeMapOverrideWithDst(map[string]string{
+ oam.TraitTypeLabel: AuxiliaryWorkload,
+ }, commonLabels), assist.Name)
+ if err != nil {
+ return nil, err
+ }
+ outputs[assist.Name] = object
+ }
+ if len(outputs) > 0 {
+ root[OutputsFieldName] = outputs
+ }
+ return root, nil
}
// HealthCheck address health check for workload
-func (wd *workloadDef) HealthCheck() error {
- if wd.health == "" {
- return nil
+func (wd *workloadDef) HealthCheck(ctx process.Context, cli client.Client, ns string, healthPolicyTemplate string) (bool, error) {
+ if healthPolicyTemplate == "" {
+ return true, nil
}
- bi := build.NewContext().NewInstance("", nil)
- if err := bi.AddFile("-", wd.health); err != nil {
- return err
+ templateContext, err := wd.getTemplateContext(ctx, cli, ns)
+ if err != nil {
+ return false, errors.WithMessage(err, "get template context")
}
- if wd.output != nil {
- bt, _ := json.Marshal(wd.output)
- if err := bi.AddFile(OutputFieldName, fmt.Sprintf("output: %s", string(bt))); err != nil {
- return err
- }
- } else {
- return errors.WithMessagef(errors.New("there is no workload output cr for health check"), "workload %s health check", wd.name)
+ return checkHealth(templateContext, healthPolicyTemplate)
+}
+
+func checkHealth(templateContext map[string]interface{}, healthPolicyTemplate string) (bool, error) {
+ bt, err := json.Marshal(templateContext)
+ if err != nil {
+ return false, errors.WithMessage(err, "json marshal template context")
}
- insts := cue.Build([]*build.Instance{bi})
- for _, inst := range insts {
- if err := inst.Value().Err(); err != nil {
- return errors.WithMessagef(err, "workload %s check", wd.name)
- }
- isHealthVal := inst.Lookup("isHealth")
- if isHealthVal.Exists() {
- healthRs := isHealthVal.Eval()
- if isHealth, err := healthRs.Bool(); err != nil || !isHealth {
- return errors.WithMessage(err, "the workload is unhealthy")
- }
- }
+
+ var buff = "context: " + string(bt) + "\n" + healthPolicyTemplate
+ var r cue.Runtime
+ inst, err := r.Compile("-", buff)
+ if err != nil {
+ return false, errors.WithMessage(err, "compile health template")
}
- return nil
+ healthy, err := inst.Lookup(HealthCheckPolicy).Bool()
+ if err != nil {
+ return false, errors.WithMessage(err, "evaluate health status")
+ }
+ return healthy, nil
+}
+
+// Status get workload status by customStatusTemplate
+func (wd *workloadDef) Status(ctx process.Context, cli client.Client, ns string, customStatusTemplate string) (string, error) {
+ if customStatusTemplate == "" {
+ return "", nil
+ }
+ templateContext, err := wd.getTemplateContext(ctx, cli, ns)
+ if err != nil {
+ return "", errors.WithMessage(err, "get template context")
+ }
+ return getStatusMessage(templateContext, customStatusTemplate)
+}
+
+func getStatusMessage(templateContext map[string]interface{}, customStatusTemplate string) (string, error) {
+ bt, err := json.Marshal(templateContext)
+ if err != nil {
+ return "", errors.WithMessage(err, "json marshal template context")
+ }
+ var buff = "context: " + string(bt) + "\n" + customStatusTemplate
+ var r cue.Runtime
+ inst, err := r.Compile("-", buff)
+ if err != nil {
+ return "", errors.WithMessage(err, "compile customStatus template")
+ }
+ message, err := inst.Lookup(CustomMessage).String()
+ if err != nil {
+ return "", errors.WithMessage(err, "evaluate customStatus.message")
+ }
+ return message, nil
}
type traitDef struct {
def
}
-// NewTDTemplater create Trait Definition templater
-func NewTDTemplater(name, templ, health string) Template {
+// NewTraitAbstractEngine create Trait Definition AbstractEngine
+func NewTraitAbstractEngine(name string) AbstractEngine {
return &traitDef{
def: def{
- name: name,
- templ: templ,
- health: health,
+ name: name,
},
}
}
// Params set definition's params
-func (td *traitDef) Params(params interface{}) Template {
+func (td *traitDef) Params(params interface{}) AbstractEngine {
td.params = params
return td
}
// Complete do trait definition's rendering
-func (td *traitDef) Complete(ctx process.Context) error {
+func (td *traitDef) Complete(ctx process.Context, abstractTemplate string) error {
bi := build.NewContext().NewInstance("", nil)
- if err := bi.AddFile("-", td.templ); err != nil {
- return err
+ if err := bi.AddFile("-", abstractTemplate); err != nil {
+ return errors.WithMessagef(err, "invalid template of trait %s", td.name)
}
if td.params != nil {
- bt, _ := json.Marshal(td.params)
+ bt, err := json.Marshal(td.params)
+ if err != nil {
+ return errors.WithMessagef(err, "marshal parameter of trait %s", td.name)
+ }
if err := bi.AddFile("parameter", fmt.Sprintf("parameter: %s", string(bt))); err != nil {
- return err
+ return errors.WithMessagef(err, "invalid parameter of trait %s", td.name)
}
}
- if err := bi.AddFile("f", ctx.Compile("context")); err != nil {
- return err
+ if err := bi.AddFile("context", ctx.BaseContextFile()); err != nil {
+ return errors.WithMessagef(err, "invalid context of trait %s", td.name)
}
- insts := cue.Build([]*build.Instance{bi})
- for _, inst := range insts {
-
+ instances := cue.Build([]*build.Instance{bi})
+ for _, inst := range instances {
if err := inst.Value().Err(); err != nil {
- return errors.WithMessagef(err, "traitDef %s build", td.name)
+ return errors.WithMessagef(err, "invalid template of trait %s after merge with parameter and context", td.name)
}
-
processing := inst.Lookup("processing")
var err error
if processing.Exists() {
if inst, err = task.Process(inst); err != nil {
- return errors.WithMessagef(err, "traitDef %s build", td.name)
+ return errors.WithMessagef(err, "invalid process of trait %s", td.name)
}
}
@@ -230,14 +295,16 @@ func (td *traitDef) Complete(ctx process.Context) error {
if output.Exists() {
other, err := model.NewOther(output)
if err != nil {
- return errors.WithMessagef(err, "traitDef %s new Assist", td.name)
+ return errors.WithMessagef(err, "invalid output of trait %s", td.name)
}
- ctx.PutAssistants(process.Assistant{Ins: other, Type: td.name})
+ ctx.AppendAuxiliaries(process.Auxiliary{Ins: other, Type: td.name, IsOutputs: false})
}
-
outputs := inst.Lookup(OutputsFieldName)
- st, err := outputs.Struct()
- if err == nil {
+ if outputs.Exists() {
+ st, err := outputs.Struct()
+ if err != nil {
+ return errors.WithMessagef(err, "invalid outputs of trait %s", td.name)
+ }
for i := 0; i < st.Len(); i++ {
fieldInfo := st.Field(i)
if fieldInfo.IsDefinition || fieldInfo.IsHidden || fieldInfo.IsOptional {
@@ -245,9 +312,9 @@ func (td *traitDef) Complete(ctx process.Context) error {
}
other, err := model.NewOther(fieldInfo.Value)
if err != nil {
- return errors.WithMessagef(err, "traitDef %s new Assists(%s)", td.name, fieldInfo.Name)
+ return errors.WithMessagef(err, "invalid outputs(resource=%s) of trait %s", fieldInfo.Name, td.name)
}
- ctx.PutAssistants(process.Assistant{Ins: other, Type: td.name})
+ ctx.AppendAuxiliaries(process.Auxiliary{Ins: other, Type: td.name, Name: fieldInfo.Name, IsOutputs: true})
}
}
@@ -256,91 +323,102 @@ func (td *traitDef) Complete(ctx process.Context) error {
base, _ := ctx.Output()
p, err := model.NewOther(patcher)
if err != nil {
- return errors.WithMessagef(err, "traitDef %s patcher NewOther", td.name)
+ return errors.WithMessagef(err, "invalid patch of trait %s", td.name)
}
if err := base.Unify(p); err != nil {
- return err
+ return errors.WithMessagef(err, "invalid patch trait %s into workload", td.name)
}
}
}
return nil
}
-// Output fetch the trait cr and set result to context
-func (td *traitDef) Output(ctx process.Context, client client.Client, name string) Template {
+func (td *traitDef) getTemplateContext(ctx process.Context, cli client.Reader, ns string) (map[string]interface{}, error) {
+ var root = map[string]interface{}{}
+ var commonLabels = map[string]string{}
+ for k, v := range ctx.BaseContextLabels() {
+ root[k] = v
+ switch k {
+ case "appName":
+ commonLabels[oam.LabelAppName] = v
+ case "name":
+ commonLabels[oam.LabelAppComponent] = v
+ }
+ }
_, assists := ctx.Output()
+ outputs := make(map[string]interface{})
for _, assist := range assists {
if assist.Type != td.name {
continue
}
traitRef, err := assist.Ins.Unstructured()
if err != nil {
- return td
+ return nil, err
}
- traitCr, err := getObj(client, traitRef, name)
+ object, err := getResourceFromObj(traitRef, cli, ns, util.MergeMapOverrideWithDst(map[string]string{
+ oam.TraitTypeLabel: assist.Type,
+ }, commonLabels), assist.Name)
if err != nil {
- return td
+ return nil, err
+ }
+ if assist.IsOutputs {
+ outputs[assist.Name] = object
+ } else {
+ root[OutputFieldName] = object
}
- td.output = traitCr
- return td
}
- return td
+ if len(outputs) > 0 {
+ root[OutputsFieldName] = outputs
+ }
+ return root, nil
+}
+
+// Status get trait status by customStatusTemplate
+func (td *traitDef) Status(ctx process.Context, cli client.Client, ns string, customStatusTemplate string) (string, error) {
+ if customStatusTemplate == "" {
+ return "", nil
+ }
+ templateContext, err := td.getTemplateContext(ctx, cli, ns)
+ if err != nil {
+ return "", errors.WithMessage(err, "get template context")
+ }
+ return getStatusMessage(templateContext, customStatusTemplate)
}
// HealthCheck address health check for trait
-func (td *traitDef) HealthCheck() error {
- if td.health == "" {
- return nil
+func (td *traitDef) HealthCheck(ctx process.Context, cli client.Client, ns string, healthPolicyTemplate string) (bool, error) {
+ if healthPolicyTemplate == "" {
+ return true, nil
}
- bi := build.NewContext().NewInstance("", nil)
- if err := bi.AddFile("-", td.health); err != nil {
- return err
+ templateContext, err := td.getTemplateContext(ctx, cli, ns)
+ if err != nil {
+ return false, errors.WithMessage(err, "get template context")
}
- if td.output != nil {
- bt, _ := json.Marshal(td.output)
- if err := bi.AddFile("output", fmt.Sprintf("output: %s", string(bt))); err != nil {
- return err
- }
- } else {
- return errors.WithMessagef(errors.New("there is no trait output cr for health check"), "trait %s health check", td.name)
- }
- insts := cue.Build([]*build.Instance{bi})
- for _, inst := range insts {
- if err := inst.Value().Err(); err != nil {
- return errors.WithMessagef(err, "trait %s check", td.name)
- }
- isHealthVal := inst.Lookup("isHealth")
- if isHealthVal.Exists() {
- if isHealth, err := isHealthVal.Bool(); err != nil || !isHealth {
- return errors.WithMessage(err, "the trait is unhealthy")
- }
- }
- }
- return nil
+ return checkHealth(templateContext, healthPolicyTemplate)
}
-func getObj(cli client.Client, obj runtime.Object, name string) (map[string]interface{}, error) {
- var kind, apiVersion string
- var err error
- kind, err = metadataAccessor.Kind(obj)
- if err != nil {
- return nil, fmt.Errorf("cannot access object kind")
+func getResourceFromObj(obj *unstructured.Unstructured, client client.Reader, namespace string, labels map[string]string, outputsResource string) (map[string]interface{}, error) {
+ if outputsResource != "" {
+ labels[oam.TraitResource] = outputsResource
}
- apiVersion, err = metadataAccessor.APIVersion(obj)
- if err != nil {
- return nil, fmt.Errorf("cannot access object kind")
- }
- unList := &unstructured.UnstructuredList{}
- unList.SetKind(kind)
- unList.SetAPIVersion(apiVersion)
- if err := cli.List(context.Background(), unList, client.MatchingLabels{oam.LabelAppName: name}); err != nil {
- if kerrors.IsNotFound(err) {
- return nil, nil
+ if obj.GetName() != "" {
+ u, err := util.GetObjectGivenGVKAndName(context.Background(), client, obj.GroupVersionKind(), namespace, obj.GetName())
+ if err != nil {
+ return nil, err
}
+ return u.Object, nil
+ }
+ list, err := util.GetObjectsGivenGVKAndLabels(context.Background(), client, obj.GroupVersionKind(), namespace, labels)
+ if err != nil {
return nil, err
}
- if len(unList.Items) == 0 {
- return nil, nil
+ if len(list.Items) == 1 {
+ return list.Items[0].Object, nil
}
- return unList.Items[0].Object, nil
+ for _, v := range list.Items {
+ if v.GetLabels()[oam.TraitResource] == outputsResource {
+ return v.Object, nil
+ }
+ }
+ return nil, errors.Errorf("no resources found gvk(%v) labels(%v)", obj.GroupVersionKind(), labels)
}
diff --git a/pkg/dsl/definition/template_test.go b/pkg/dsl/definition/template_test.go
index ddaef151b..5a252a19a 100644
--- a/pkg/dsl/definition/template_test.go
+++ b/pkg/dsl/definition/template_test.go
@@ -3,60 +3,217 @@ package definition
import (
"testing"
- "github.com/bmizerany/assert"
+ "github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/oam-dev/kubevela/pkg/dsl/process"
)
-func TestWDTemplate(t *testing.T) {
+func TestWorkloadTemplateComplete(t *testing.T) {
testCases := []struct {
- templ string
- params map[string]interface{}
- expectObj runtime.Object
+ workloadTemplate string
+ params map[string]interface{}
+ expectObj runtime.Object
+ expAssObjs map[string]runtime.Object
}{
{
- templ: `
+ workloadTemplate: `
output:{
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: context.name
spec: replicas: parameter.replicas
}
-
parameter: {
replicas: *1 | int
+ type: string
+ host: string
}
`,
params: map[string]interface{}{
"replicas": 2,
+ "type": "ClusterIP",
+ "host": "example.com",
},
expectObj: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"replicas": int64(2)}}},
},
+ {
+ workloadTemplate: `
+output:{
+ apiVersion: "apps/v1"
+ kind: "Deployment"
+ metadata: name: context.name
+ spec: replicas: parameter.replicas
+}
+outputs: service: {
+ apiVersion: "v1"
+ kind: "Service"
+ metadata: name: context.name
+ spec: type: parameter.type
+}
+outputs: ingress: {
+ apiVersion: "extensions/v1beta1"
+ kind: "Ingress"
+ metadata: name: context.name
+ spec: rules: [{host: parameter.host}]
+}
+
+parameter: {
+ replicas: *1 | int
+ type: string
+ host: string
+}
+`,
+ params: map[string]interface{}{
+ "replicas": 2,
+ "type": "ClusterIP",
+ "host": "example.com",
+ },
+ expectObj: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"replicas": int64(2)}}},
+ expAssObjs: map[string]runtime.Object{
+ "service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"type": "ClusterIP"}}},
+ "ingress": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "extensions/v1beta1", "kind": "Ingress", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"rules": []interface{}{map[string]interface{}{
+ "host": "example.com",
+ }}}}},
+ },
+ },
}
for _, v := range testCases {
- ctx := process.NewContext("test")
- wt := NewWDTemplater("-", v.templ, "")
- if err := wt.Params(v.params).Complete(ctx); err != nil {
- t.Error(err)
- return
- }
+ ctx := process.NewContext("test", "myapp")
+ wt := NewWorkloadAbstractEngine("testworkload")
+ assert.NoError(t, wt.Params(v.params).Complete(ctx, v.workloadTemplate))
base, assists := ctx.Output()
- assert.Equal(t, 0, len(assists))
- assert.Equal(t, false, base == nil)
+ assert.Equal(t, len(v.expAssObjs), len(assists))
+ assert.NotNil(t, base)
baseObj, err := base.Unstructured()
assert.Equal(t, nil, err)
assert.Equal(t, v.expectObj, baseObj)
-
+ for _, ss := range assists {
+ assert.Equal(t, AuxiliaryWorkload, ss.Type)
+ got, err := ss.Ins.Unstructured()
+ assert.NoError(t, err)
+ assert.Equal(t, got, v.expAssObjs[ss.Name])
+ }
}
}
-func TestTDTemplate(t *testing.T) {
- baseTemplate := `
+func TestTraitTemplateComplete(t *testing.T) {
+
+ tds := map[string]struct {
+ traitName string
+ traitTemplate string
+ params map[string]interface{}
+ expWorkload *unstructured.Unstructured
+ expAssObjs map[string]runtime.Object
+ }{
+ "patch trait": {
+ traitTemplate: `
+patch: {
+ // +patchKey=name
+ spec: template: spec: containers: [parameter]
+}
+
+parameter: {
+ name: string
+ image: string
+ command?: [...string]
+}`,
+ params: map[string]interface{}{
+ "name": "sidecar",
+ "image": "metrics-agent:0.2",
+ },
+ expWorkload: &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "apps/v1",
+ "kind": "Deployment",
+ "metadata": map[string]interface{}{"name": "test"},
+ "spec": map[string]interface{}{
+ "replicas": int64(2),
+ "template": map[string]interface{}{
+ "spec": map[string]interface{}{
+ "containers": []interface{}{map[string]interface{}{"image": "website:0.1", "name": "main"},
+ map[string]interface{}{"image": "metrics-agent:0.2", "name": "sidecar"}}}}},
+ }},
+ },
+ "output trait": {
+ traitTemplate: `
+output: {
+ apiVersion: "v1"
+ kind: "Service"
+ metadata: name: context.name
+ spec: type: parameter.type
+}
+parameter: {
+ type: string
+}`,
+ params: map[string]interface{}{
+ "type": "ClusterIP",
+ },
+ expWorkload: &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "apps/v1",
+ "kind": "Deployment",
+ "metadata": map[string]interface{}{"name": "test"},
+ "spec": map[string]interface{}{
+ "replicas": int64(2),
+ "template": map[string]interface{}{
+ "spec": map[string]interface{}{
+ "containers": []interface{}{map[string]interface{}{"image": "website:0.1", "name": "main"}}}}},
+ }},
+ traitName: "t1",
+ expAssObjs: map[string]runtime.Object{
+ "t1": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"type": "ClusterIP"}}},
+ },
+ },
+ "outputs trait": {
+ traitTemplate: `
+output: {
+ apiVersion: "v1"
+ kind: "Service"
+ metadata: name: context.name
+ spec: type: parameter.type
+}
+outputs: ingress: {
+ apiVersion: "extensions/v1beta1"
+ kind: "Ingress"
+ metadata: name: context.name
+ spec: rules: [{host: parameter.host}]
+}
+parameter: {
+ type: string
+ host: string
+}`,
+ params: map[string]interface{}{
+ "type": "ClusterIP",
+ "host": "example.com",
+ },
+ expWorkload: &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "apps/v1",
+ "kind": "Deployment",
+ "metadata": map[string]interface{}{"name": "test"},
+ "spec": map[string]interface{}{
+ "replicas": int64(2),
+ "template": map[string]interface{}{
+ "spec": map[string]interface{}{
+ "containers": []interface{}{map[string]interface{}{"image": "website:0.1", "name": "main"}}}}},
+ }},
+ traitName: "t2",
+ expAssObjs: map[string]runtime.Object{
+ "t2": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"type": "ClusterIP"}}},
+ "t2ingress": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "extensions/v1beta1", "kind": "Ingress", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"rules": []interface{}{map[string]interface{}{
+ "host": "example.com",
+ }}}}},
+ },
+ },
+ }
+
+ for cassinfo, v := range tds {
+ baseTemplate := `
output:{
apiVersion: "apps/v1"
kind: "Deployment"
@@ -73,63 +230,148 @@ parameter: {
replicas: *1 | int
}
`
- ctx := process.NewContext("test")
- wt := NewWDTemplater("-", baseTemplate, "")
- if err := wt.Params(map[string]interface{}{
- "replicas": 2,
- }).Complete(ctx); err != nil {
- t.Error(err)
- return
- }
-
- tds := []struct {
- templ string
- params map[string]interface{}
- }{
- {
- templ: `
-patch: {
- // +patchKey=name
- spec: template: spec: containers: [parameter]
-}
-
-parameter: {
- name: string
- image: string
- command?: [...string]
-}
-`,
- params: map[string]interface{}{
- "name": "sidecar",
- "image": "metrics-agent:0.2",
- },
- },
- }
-
- for _, v := range tds {
- td := NewTDTemplater("-", v.templ, "")
- if err := td.Params(v.params).Complete(ctx); err != nil {
+ ctx := process.NewContext("test", "myapp")
+ wt := NewWorkloadAbstractEngine("-")
+ if err := wt.Params(map[string]interface{}{
+ "replicas": 2,
+ }).Complete(ctx, baseTemplate); err != nil {
t.Error(err)
return
}
+ td := NewTraitAbstractEngine(v.traitName)
+ assert.NoError(t, td.Params(v.params).Complete(ctx, v.traitTemplate))
+ base, assists := ctx.Output()
+ assert.Equal(t, len(v.expAssObjs), len(assists), cassinfo)
+ assert.NotNil(t, base)
+ obj, err := base.Unstructured()
+ assert.NoError(t, err)
+ assert.Equal(t, v.expWorkload, obj, cassinfo)
+ for _, ss := range assists {
+ got, err := ss.Ins.Unstructured()
+ assert.NoError(t, err, cassinfo)
+ assert.Equal(t, got, v.expAssObjs[ss.Type+ss.Name], cassinfo, ss.Type+ss.Name)
+ }
+ }
+}
+
+func TestCheckHealth(t *testing.T) {
+ cases := map[string]struct {
+ tpContext map[string]interface{}
+ healthTemp string
+ exp bool
+ }{
+ "normal-equal": {
+ tpContext: map[string]interface{}{
+ "output": map[string]interface{}{
+ "status": map[string]interface{}{
+ "readyReplicas": 4,
+ "replicas": 4,
+ },
+ },
+ },
+ healthTemp: "isHealth: context.output.status.readyReplicas == context.output.status.replicas",
+ exp: true,
+ },
+ "normal-false": {
+ tpContext: map[string]interface{}{
+ "output": map[string]interface{}{
+ "status": map[string]interface{}{
+ "readyReplicas": 4,
+ "replicas": 5,
+ },
+ },
+ },
+ healthTemp: "isHealth: context.output.status.readyReplicas == context.output.status.replicas",
+ exp: false,
+ },
+ "array-case-equal": {
+ tpContext: map[string]interface{}{
+ "output": map[string]interface{}{
+ "status": map[string]interface{}{
+ "conditions": []interface{}{
+ map[string]interface{}{"status": "True"},
+ },
+ },
+ },
+ },
+ healthTemp: `isHealth: context.output.status.conditions[0].status == "True"`,
+ exp: true,
+ },
+ }
+ for message, ca := range cases {
+ healthy, err := checkHealth(ca.tpContext, ca.healthTemp)
+ assert.NoError(t, err, message)
+ assert.Equal(t, ca.exp, healthy, message)
+ }
+}
+
+func TestGetStatus(t *testing.T) {
+ cases := map[string]struct {
+ tpContext map[string]interface{}
+ statusTemp string
+ expMessage string
+ }{
+ "field-with-array-and-outputs": {
+ tpContext: map[string]interface{}{
+ "outputs": map[string]interface{}{
+ "service": map[string]interface{}{
+ "spec": map[string]interface{}{
+ "type": "NodePort",
+ "clusterIP": "10.0.0.1",
+ "ports": []interface{}{
+ map[string]interface{}{
+ "port": 80,
+ },
+ },
+ },
+ },
+ "ingress": map[string]interface{}{
+ "rules": []interface{}{
+ map[string]interface{}{
+ "host": "example.com",
+ },
+ },
+ },
+ },
+ },
+ statusTemp: `message: "type: " + context.outputs.service.spec.type + " clusterIP:" + context.outputs.service.spec.clusterIP + " ports:" + "\(context.outputs.service.spec.ports[0].port)" + " domain:" + context.outputs.ingress.rules[0].host`,
+ expMessage: "type: NodePort clusterIP:10.0.0.1 ports:80 domain:example.com",
+ },
+ "complex status": {
+ tpContext: map[string]interface{}{
+ "outputs": map[string]interface{}{
+ "ingress": map[string]interface{}{
+ "spec": map[string]interface{}{
+ "rules": []interface{}{
+ map[string]interface{}{
+ "host": "example.com",
+ },
+ },
+ },
+ "status": map[string]interface{}{
+ "loadBalancer": map[string]interface{}{
+ "ingress": []interface{}{
+ map[string]interface{}{
+ "ip": "10.0.0.1",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ statusTemp: `if len(context.outputs.ingress.status.loadBalancer.ingress) > 0 {
+ message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + context.outputs.ingress.status.loadBalancer.ingress[0].ip
+}
+if len(context.outputs.ingress.status.loadBalancer.ingress) == 0 {
+ message: "No loadBalancer found, visiting by using 'vela port-forward " + context.appName + " --route'\n"
+}`,
+ expMessage: "Visiting URL: example.com, IP: 10.0.0.1",
+ },
+ }
+ for message, ca := range cases {
+ gotMessage, err := getStatusMessage(ca.tpContext, ca.statusTemp)
+ assert.NoError(t, err, message)
+ assert.Equal(t, ca.expMessage, gotMessage, message)
}
-
- base, assists := ctx.Output()
- assert.Equal(t, 0, len(assists))
- assert.Equal(t, false, base == nil)
- obj, err := base.Unstructured()
- assert.Equal(t, nil, err)
- expect := &unstructured.Unstructured{
- Object: map[string]interface{}{
- "apiVersion": "apps/v1",
- "kind": "Deployment",
- "metadata": map[string]interface{}{"name": "test"},
- "spec": map[string]interface{}{
- "replicas": int64(2),
- "template": map[string]interface{}{
- "spec": map[string]interface{}{
- "containers": []interface{}{map[string]interface{}{"image": "website:0.1", "name": "main"},
- map[string]interface{}{"image": "metrics-agent:0.2", "name": "sidecar"}}}}},
- }}
- assert.Equal(t, expect, obj)
}
diff --git a/pkg/dsl/process/handle.go b/pkg/dsl/process/handle.go
index ef2d91edd..581c362a1 100644
--- a/pkg/dsl/process/handle.go
+++ b/pkg/dsl/process/handle.go
@@ -12,55 +12,72 @@ import (
// Context defines Rendering Context Interface
type Context interface {
SetBase(base model.Instance)
- PutAssistants(insts ...Assistant)
+ AppendAuxiliaries(auxiliaries ...Auxiliary)
SetConfigs(configs []map[string]string)
- Output() (model.Instance, []Assistant)
- Compile(label string) string
+ Output() (model.Instance, []Auxiliary)
+ BaseContextFile() string
+ BaseContextLabels() map[string]string
}
-// Assistant are objects rendered by definition template.
-type Assistant struct {
+// Auxiliary are objects rendered by definition template.
+type Auxiliary struct {
Ins model.Instance
// Type will be used to mark definition label for OAM runtime to get the CRD
// It's now required for trait and main workload object. Extra workload CR object will not have the type.
Type string
+
+ // Workload or trait with multiple `outputs` will have a name, if name is empty, than it's the main of this type.
+ Name string
+
+ // IsOutputs will record the output path format of the Auxiliary
+ // it can be one of these two cases:
+ // false: the format is `output`, this means it's the main resource of the trait
+ // true: the format is `outputs.`, this means it can be auxiliary workload or trait
+ IsOutputs bool
}
-type context struct {
- name string
- configs []map[string]string
- base model.Instance
- assistants []Assistant
+type templateContext struct {
+ // name is the component name of Application
+ name string
+ // appName is the name of Application
+ appName string
+ configs []map[string]string
+ base model.Instance
+ auxiliaries []Auxiliary
+
+ // TODO(wonderflow): add a revision number here, and it should be a suffix combined with appName to be the name of AppConfig
}
-// NewContext create render context
-func NewContext(name string) Context {
- return &context{
- name: name,
- configs: []map[string]string{},
- assistants: []Assistant{},
+// NewContext create render templateContext
+func NewContext(name, appName string) Context {
+ return &templateContext{
+ name: name,
+ appName: appName,
+ configs: []map[string]string{},
+ auxiliaries: []Auxiliary{},
}
}
-// SetBase set context base model
-func (ctx *context) SetConfigs(configs []map[string]string) {
+// SetBase set templateContext base model
+func (ctx *templateContext) SetConfigs(configs []map[string]string) {
ctx.configs = configs
}
-// SetBase set context base model
-func (ctx *context) SetBase(base model.Instance) {
+// SetBase set templateContext base model
+func (ctx *templateContext) SetBase(base model.Instance) {
ctx.base = base
}
-// PutAssistants add Assist model to context
-func (ctx *context) PutAssistants(insts ...Assistant) {
- ctx.assistants = append(ctx.assistants, insts...)
+// AppendAuxiliaries add Assist model to templateContext
+func (ctx *templateContext) AppendAuxiliaries(auxiliaries ...Auxiliary) {
+ ctx.auxiliaries = append(ctx.auxiliaries, auxiliaries...)
}
-// Compile return cue format string of context
-func (ctx *context) Compile(label string) string {
+// BaseContextFile return cue format string of templateContext
+func (ctx *templateContext) BaseContextFile() string {
var buff string
buff += fmt.Sprintf("name: \"%s\"\n", ctx.name)
+ buff += fmt.Sprintf("appName: \"%s\"\n", ctx.appName)
if ctx.base != nil {
buff += fmt.Sprintf("input: %s\n", structMarshal(ctx.base.String()))
@@ -71,16 +88,22 @@ func (ctx *context) Compile(label string) string {
buff += "config: " + string(bt)
}
- if label != "" {
- buff = fmt.Sprintf("%s: %s", label, structMarshal(buff))
- }
-
- return buff
+ return fmt.Sprintf("context: %s", structMarshal(buff))
}
-// Output return models of context
-func (ctx *context) Output() (model.Instance, []Assistant) {
- return ctx.base, ctx.assistants
+func (ctx *templateContext) BaseContextLabels() map[string]string {
+
+ return map[string]string{
+ // appName is oam.LabelAppName
+ "appName": ctx.appName,
+ // name is oam.LabelAppComponent
+ "name": ctx.name,
+ }
+}
+
+// GetK8sResource return models of templateContext
+func (ctx *templateContext) Output() (model.Instance, []Auxiliary) {
+ return ctx.base, ctx.auxiliaries
}
func structMarshal(v string) string {
diff --git a/pkg/dsl/process/handle_test.go b/pkg/dsl/process/handle_test.go
index 2a4b2c3b4..b1aa264c8 100644
--- a/pkg/dsl/process/handle_test.go
+++ b/pkg/dsl/process/handle_test.go
@@ -26,9 +26,9 @@ image: "myserver"
return
}
- ctx := NewContext("myctx")
+ ctx := NewContext("mycomp", "myapp")
ctx.SetBase(base)
- ctxInst, err := r.Compile("-", ctx.Compile("context"))
+ ctxInst, err := r.Compile("-", ctx.BaseContextFile())
if err != nil {
t.Error(err)
return
@@ -36,7 +36,11 @@ image: "myserver"
gName, err := ctxInst.Lookup("context", "name").String()
assert.Equal(t, nil, err)
- assert.Equal(t, "myctx", gName)
+ assert.Equal(t, "mycomp", gName)
+
+ myAppName, err := ctxInst.Lookup("context", "appName").String()
+ assert.Equal(t, nil, err)
+ assert.Equal(t, "myapp", myAppName)
inputJs, err := ctxInst.Lookup("context", "input").MarshalJSON()
assert.Equal(t, nil, err)
assert.Equal(t, `{"image":"myserver"}`, string(inputJs))
diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go
index 844dc0107..929554346 100644
--- a/pkg/oam/labels.go
+++ b/pkg/oam/labels.go
@@ -32,6 +32,8 @@ const (
WorkloadTypeLabel = "workload.oam.dev/type"
// TraitTypeLabel indicates the type of the traitDefinition
TraitTypeLabel = "trait.oam.dev/type"
+ // TraitResource indicates which resource it is when a trait is composed by multiple resources in KubeVela
+ TraitResource = "trait.oam.dev/resource"
)
const (
diff --git a/pkg/oam/util/helper.go b/pkg/oam/util/helper.go
index a79d85e26..81387aeb3 100644
--- a/pkg/oam/util/helper.go
+++ b/pkg/oam/util/helper.go
@@ -11,8 +11,6 @@ import (
"strings"
"time"
- "k8s.io/apimachinery/pkg/runtime"
-
cpv1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/davecgh/go-spew/spew"
"github.com/go-logr/logr"
@@ -23,6 +21,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
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/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
@@ -355,6 +354,22 @@ func GetGVKFromDefinition(dm discoverymapper.DiscoveryMapper, definitionRef v1al
return kinds[0], nil
}
+// GetObjectsGivenGVKAndLabels fetches the kubernetes object given its gvk and labels by list API
+func GetObjectsGivenGVKAndLabels(ctx context.Context, cli client.Reader,
+ gvk schema.GroupVersionKind, namespace string, labels map[string]string) (*unstructured.UnstructuredList, error) {
+ unstructuredObjList := &unstructured.UnstructuredList{}
+ apiVersion := metav1.GroupVersion{
+ Group: gvk.Group,
+ Version: gvk.Version,
+ }.String()
+ unstructuredObjList.SetAPIVersion(apiVersion)
+ unstructuredObjList.SetKind(gvk.Kind)
+ if err := cli.List(ctx, unstructuredObjList, client.MatchingLabels(labels), client.InNamespace(namespace)); err != nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("failed to get obj with labels %+v and gvk %+v ", labels, gvk))
+ }
+ return unstructuredObjList, nil
+}
+
// GetObjectGivenGVKAndName fetches the kubernetes object given its gvk and name
func GetObjectGivenGVKAndName(ctx context.Context, client client.Reader,
gvk schema.GroupVersionKind, namespace, name string) (*unstructured.Unstructured, error) {
diff --git a/pkg/oam/util/template.go b/pkg/oam/util/template.go
index 3149c3a37..6d16e7e00 100644
--- a/pkg/oam/util/template.go
+++ b/pkg/oam/util/template.go
@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/pkg/errors"
+ "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -18,6 +19,7 @@ import (
type Template struct {
TemplateStr string
Health string
+ CustomStatus string
CapabilityCategory types.CapabilityCategory
}
@@ -46,7 +48,7 @@ func LoadTemplate(cli client.Reader, key string, kd types.CapType) (*Template, e
if wd.Annotations["type"] == string(types.TerraformCategory) {
capabilityCategory = types.TerraformCategory
}
- tmpl, err := getTemplAndHealth(wd.Spec.Extension.Raw)
+ tmpl, err := NewTemplate(wd.Spec.Template, wd.Spec.Status, wd.Spec.Extension)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
}
@@ -65,7 +67,7 @@ func LoadTemplate(cli client.Reader, key string, kd types.CapType) (*Template, e
if td.Annotations["type"] == string(types.TerraformCategory) {
capabilityCategory = types.TerraformCategory
}
- tmpl, err := getTemplAndHealth(td.Spec.Extension.Raw)
+ tmpl, err := NewTemplate(td.Spec.Template, td.Spec.Status, td.Spec.Extension)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
}
@@ -77,19 +79,49 @@ func LoadTemplate(cli client.Reader, key string, kd types.CapType) (*Template, e
case types.TypeScope:
// TODO: add scope template support
}
-
return nil, fmt.Errorf("kind(%s) of %s not supported", kd, key)
}
-func getTemplAndHealth(raw []byte) (*Template, error) {
- _tmp := map[string]interface{}{}
- if err := json.Unmarshal(raw, &_tmp); err != nil {
- return nil, err
+// NewTemplate will create CUE template for inner AbstractEngine using.
+func NewTemplate(template string, status *v1alpha2.Status, raw *runtime.RawExtension) (*Template, error) {
+ extension := map[string]interface{}{}
+ tmp := &Template{
+ TemplateStr: template,
}
- var health string
- if _, ok := _tmp["healthPolicy"]; ok {
- health = fmt.Sprint(_tmp["healthPolicy"])
+ if tmp.TemplateStr == "" && raw != nil {
+ if err := json.Unmarshal(raw.Raw, &extension); err != nil {
+ return nil, err
+ }
+ if extTemplate, ok := extension["template"]; ok {
+ if tmpStr, ok := extTemplate.(string); ok {
+ tmp.TemplateStr = tmpStr
+ }
+ }
}
- return &Template{TemplateStr: fmt.Sprint(_tmp["template"]),
- Health: health}, nil
+ if status != nil {
+ tmp.CustomStatus = status.CustomStatus
+ tmp.Health = status.HealthPolicy
+ }
+ return tmp, nil
+}
+
+// ConvertTemplateJSON2Object convert spec.extension to object
+func ConvertTemplateJSON2Object(in *runtime.RawExtension, specTemplate string) (types.Capability, error) {
+ var t types.Capability
+ capTemplate, err := NewTemplate(specTemplate, nil, in)
+ if err != nil {
+ return t, errors.Wrapf(err, "parse cue template")
+ }
+ var extension types.Capability
+ if in != nil && in.Raw != nil {
+ err := json.Unmarshal(in.Raw, &extension)
+ if err != nil {
+ return t, errors.Wrapf(err, "parse extension fail")
+ }
+ t = extension
+ }
+ if capTemplate.TemplateStr != "" {
+ t.CueTemplate = capTemplate.TemplateStr
+ }
+ return t, err
}
diff --git a/pkg/oam/util/template_test.go b/pkg/oam/util/template_test.go
index 63ee0a818..a667f2313 100644
--- a/pkg/oam/util/template_test.go
+++ b/pkg/oam/util/template_test.go
@@ -4,6 +4,8 @@ import (
"context"
"testing"
+ "github.com/stretchr/testify/assert"
+
"cuelang.org/go/cue"
"github.com/crossplane/crossplane-runtime/pkg/test"
"k8s.io/apimachinery/pkg/runtime"
@@ -13,7 +15,7 @@ import (
"github.com/oam-dev/kubevela/apis/types"
)
-func TestTemplate(t *testing.T) {
+func TestLoadWorkloadTemplate(t *testing.T) {
cueTemplate := `
context: {
name: "test"
@@ -110,3 +112,173 @@ spec:
t.Errorf("parsered template is not correct")
}
}
+
+func TestLoadTraitTemplate(t *testing.T) {
+ cueTemplate := `
+ parameter: {
+ domain: string
+ http: [string]: int
+ }
+ context: {
+ name: "test"
+ }
+ // 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
+ }
+ },
+ ]
+ }
+ }]
+ }
+ }
+ `
+
+ var traitDefintion = `
+apiVersion: core.oam.dev/v1alpha2
+kind: TraitDefinition
+metadata:
+ annotations:
+ 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
+spec:
+ status:
+ customStatus: |-
+ if len(context.outputs.ingress.status.loadBalancer.ingress) > 0 {
+ message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + context.outputs.ingress.status.loadBalancer.ingress[0].ip
+ }
+ if len(context.outputs.ingress.status.loadBalancer.ingress) == 0 {
+ message: "No loadBalancer found, visiting by using 'vela port-forward " + context.appName + " --route'\n"
+ }
+ healthPolicy: |
+ isHealth: len(context.outputs.service.spec.clusterIP) > 0
+ appliesToWorkloads:
+ - webservice
+ - worker
+ template: |
+` + cueTemplate
+
+ // Create mock client
+ tclient := test.MockClient{
+ MockGet: func(ctx context.Context, key ktypes.NamespacedName, obj runtime.Object) error {
+ switch o := obj.(type) {
+ case *v1alpha2.TraitDefinition:
+ wd, err := UnMarshalStringToTraitDefinition(traitDefintion)
+ if err != nil {
+ return err
+ }
+ *o = *wd
+ }
+ return nil
+ },
+ }
+
+ temp, err := LoadTemplate(&tclient, "ingress", types.TypeTrait)
+
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ var r cue.Runtime
+ inst, err := r.Compile("-", temp.TemplateStr)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ instDest, err := r.Compile("-", cueTemplate)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ s1, _ := inst.Value().String()
+ s2, _ := instDest.Value().String()
+ if s1 != s2 {
+ t.Errorf("parsered template is not correct")
+ }
+}
+
+func TestNewTemplate(t *testing.T) {
+ testCases := map[string]struct {
+ tmp string
+ status *v1alpha2.Status
+ ext *runtime.RawExtension
+ exp *Template
+ }{
+ "only tmp": {
+ tmp: "t1",
+ exp: &Template{
+ TemplateStr: "t1",
+ },
+ },
+ "no tmp,but has extension": {
+ ext: &runtime.RawExtension{Raw: []byte(`{"template":"t1"}`)},
+ exp: &Template{
+ TemplateStr: "t1",
+ },
+ },
+ "no tmp,but has extension without temp": {
+ ext: &runtime.RawExtension{Raw: []byte(`{"template":{"t1":"t2"}}`)},
+ exp: &Template{
+ TemplateStr: "",
+ },
+ },
+ "tmp with status": {
+ tmp: "t1",
+ status: &v1alpha2.Status{
+ CustomStatus: "s1",
+ HealthPolicy: "h1",
+ },
+ exp: &Template{
+ TemplateStr: "t1",
+ CustomStatus: "s1",
+ Health: "h1",
+ },
+ },
+ "no tmp only status": {
+ status: &v1alpha2.Status{
+ CustomStatus: "s1",
+ HealthPolicy: "h1",
+ },
+ exp: &Template{
+ CustomStatus: "s1",
+ Health: "h1",
+ },
+ },
+ }
+ for reason, casei := range testCases {
+ gtmp, err := NewTemplate(casei.tmp, casei.status, casei.ext)
+ assert.NoError(t, err, reason)
+ assert.Equal(t, gtmp, casei.exp, reason)
+ }
+}
diff --git a/pkg/plugins/capcenter.go b/pkg/plugins/capcenter.go
index 190f85862..96b5f67bd 100644
--- a/pkg/plugins/capcenter.go
+++ b/pkg/plugins/capcenter.go
@@ -175,14 +175,14 @@ func ParseAndSyncCapability(data []byte, syncDir string) (types.Capability, erro
if err != nil {
return types.Capability{}, err
}
- return HandleDefinition(rd.Name, syncDir, rd.Spec.Reference.Name, rd.Annotations, rd.Spec.Extension, types.TypeWorkload, nil)
+ return HandleDefinition(rd.Name, syncDir, rd.Spec.Reference.Name, rd.Annotations, rd.Spec.Extension, types.TypeWorkload, nil, rd.Spec.Template)
case "TraitDefinition":
var td v1alpha2.TraitDefinition
err = yaml.Unmarshal(data, &td)
if err != nil {
return types.Capability{}, err
}
- return HandleDefinition(td.Name, syncDir, td.Spec.Reference.Name, td.Annotations, td.Spec.Extension, types.TypeTrait, td.Spec.AppliesToWorkloads)
+ return HandleDefinition(td.Name, syncDir, td.Spec.Reference.Name, td.Annotations, td.Spec.Extension, types.TypeTrait, td.Spec.AppliesToWorkloads, td.Spec.Template)
case "ScopeDefinition":
// TODO(wonderflow): support scope definition here.
}
@@ -247,9 +247,9 @@ func (g *GithubCenter) SyncCapabilityFromCenter() error {
continue
}
//nolint:gosec
- err = ioutil.WriteFile(filepath.Join(repoDir, tmp.CrdName+".yaml"), data, 0644)
+ err = ioutil.WriteFile(filepath.Join(repoDir, tmp.Name+".yaml"), data, 0644)
if err != nil {
- fmt.Printf("write definition %s to %s err %v\n", tmp.CrdName+".yaml", repoDir, err)
+ fmt.Printf("write definition %s to %s err %v\n", tmp.Name+".yaml", repoDir, err)
continue
}
success++
diff --git a/pkg/plugins/cluster.go b/pkg/plugins/cluster.go
index 7bed52719..52e09e5ee 100644
--- a/pkg/plugins/cluster.go
+++ b/pkg/plugins/cluster.go
@@ -61,7 +61,7 @@ func GetWorkloadsFromCluster(ctx context.Context, namespace string, c types.Args
var templateErrors []error
for _, wd := range workloadDefs.Items {
- tmp, err := HandleDefinition(wd.Name, syncDir, wd.Spec.Reference.Name, wd.Annotations, wd.Spec.Extension, types.TypeWorkload, nil)
+ tmp, err := HandleDefinition(wd.Name, syncDir, wd.Spec.Reference.Name, wd.Annotations, wd.Spec.Extension, types.TypeWorkload, nil, wd.Spec.Template)
if err != nil {
templateErrors = append(templateErrors, errors.Wrapf(err, "handle workload template `%s` failed", wd.Name))
continue
@@ -93,7 +93,7 @@ func GetTraitsFromCluster(ctx context.Context, namespace string, c types.Args, s
var templateErrors []error
for _, td := range traitDefs.Items {
- tmp, err := HandleDefinition(td.Name, syncDir, td.Spec.Reference.Name, td.Annotations, td.Spec.Extension, types.TypeTrait, td.Spec.AppliesToWorkloads)
+ tmp, err := HandleDefinition(td.Name, syncDir, td.Spec.Reference.Name, td.Annotations, td.Spec.Extension, types.TypeTrait, td.Spec.AppliesToWorkloads, td.Spec.Template)
if err != nil {
templateErrors = append(templateErrors, errors.Wrapf(err, "handle trait template `%s` failed", td.Name))
continue
@@ -134,9 +134,9 @@ func validateCapabilities(tmp types.Capability, dm discoverymapper.DiscoveryMapp
}
// HandleDefinition will handle definition to capability
-func HandleDefinition(name, syncDir, crdName string, annotation map[string]string, extension *runtime.RawExtension, tp types.CapType, applyTo []string) (types.Capability, error) {
+func HandleDefinition(name, syncDir, crdName string, annotation map[string]string, extension *runtime.RawExtension, tp types.CapType, applyTo []string, template string) (types.Capability, error) {
var tmp types.Capability
- tmp, err := HandleTemplate(extension, name, syncDir)
+ tmp, err := HandleTemplate(extension, template, name, syncDir)
if err != nil {
return types.Capability{}, err
}
@@ -162,31 +162,31 @@ func GetDescription(annotation map[string]string) string {
}
// HandleTemplate will handle definition template to capability
-func HandleTemplate(in *runtime.RawExtension, name, syncDir string) (types.Capability, error) {
- tmp, err := types.ConvertTemplateJSON2Object(in)
+func HandleTemplate(in *runtime.RawExtension, specTemplate, name, syncDir string) (types.Capability, error) {
+ tmp, err := util.ConvertTemplateJSON2Object(in, specTemplate)
if err != nil {
return types.Capability{}, err
}
tmp.Name = name
-
- var cueTemplate string
+ // if spec.template is not empty it should has the highest priority
+ if specTemplate != "" {
+ tmp.CueTemplate = specTemplate
+ tmp.CueTemplateURI = ""
+ }
if tmp.CueTemplateURI != "" {
b, err := common.HTTPGet(context.Background(), tmp.CueTemplateURI)
if err != nil {
return types.Capability{}, err
}
- cueTemplate = string(b)
- tmp.CueTemplate = cueTemplate
- } else {
- if tmp.CueTemplate == "" {
- return types.Capability{}, errors.New("template not exist in definition")
- }
- cueTemplate = tmp.CueTemplate
+ tmp.CueTemplate = string(b)
+ }
+ if tmp.CueTemplate == "" {
+ return types.Capability{}, errors.New("template not exist in definition")
}
_, _ = system.CreateIfNotExist(syncDir)
filePath := filepath.Join(syncDir, name+".cue")
//nolint:gosec
- err = ioutil.WriteFile(filePath, []byte(cueTemplate), 0644)
+ err = ioutil.WriteFile(filePath, []byte(tmp.CueTemplate), 0644)
if err != nil {
return types.Capability{}, err
}
@@ -245,7 +245,7 @@ func SyncDefinitionToLocal(ctx context.Context, c types.Args, localDefinitionDir
}
if foundCapability {
template, err := HandleDefinition(capabilityName, localDefinitionDir, workloadDef.Spec.Reference.Name,
- workloadDef.Annotations, workloadDef.Spec.Extension, types.TypeWorkload, nil)
+ workloadDef.Annotations, workloadDef.Spec.Extension, types.TypeWorkload, nil, workloadDef.Spec.Template)
if err == nil {
return &template, nil
}
@@ -259,7 +259,7 @@ func SyncDefinitionToLocal(ctx context.Context, c types.Args, localDefinitionDir
}
if foundCapability {
template, err := HandleDefinition(capabilityName, localDefinitionDir, traitDef.Spec.Reference.Name,
- traitDef.Annotations, traitDef.Spec.Extension, types.TypeTrait, nil)
+ traitDef.Annotations, traitDef.Spec.Extension, types.TypeTrait, nil, workloadDef.Spec.Template)
if err == nil {
return &template, nil
}
diff --git a/pkg/serverlib/capability.go b/pkg/serverlib/capability.go
index 69e20f222..cff295b75 100644
--- a/pkg/serverlib/capability.go
+++ b/pkg/serverlib/capability.go
@@ -91,9 +91,9 @@ func InstallCapability(client client.Client, mapper discoverymapper.DiscoveryMap
switch tp.Type {
case types.TypeWorkload:
var wd v1alpha2.WorkloadDefinition
- workloadData, err := ioutil.ReadFile(filepath.Clean(filepath.Join(repoDir, tp.CrdName+".yaml")))
+ workloadData, err := ioutil.ReadFile(filepath.Clean(filepath.Join(repoDir, tp.Name+".yaml")))
if err != nil {
- return nil
+ return err
}
if err = yaml.Unmarshal(workloadData, &wd); err != nil {
return err
@@ -119,9 +119,9 @@ func InstallCapability(client client.Client, mapper discoverymapper.DiscoveryMap
}
case types.TypeTrait:
var td v1alpha2.TraitDefinition
- traitdata, err := ioutil.ReadFile(filepath.Clean(filepath.Join(repoDir, tp.CrdName+".yaml")))
+ traitdata, err := ioutil.ReadFile(filepath.Clean(filepath.Join(repoDir, tp.Name+".yaml")))
if err != nil {
- return nil
+ return err
}
if err = yaml.Unmarshal(traitdata, &td); err != nil {
return err
@@ -191,7 +191,7 @@ func GetCapabilityFromCenter(repoName, addonName string) (types.Capability, erro
return t, nil
}
}
- return types.Capability{}, fmt.Errorf("%s/%s not exist, try vela cap:center:sync %s to sync from remote", repoName, addonName, repoName)
+ return types.Capability{}, fmt.Errorf("%s/%s not exist, try 'vela cap center sync %s' to sync from remote", repoName, addonName, repoName)
}
// ListCapabilityCenters will list all capabilities from center
@@ -303,13 +303,17 @@ func uninstallCap(client client.Client, cap types.Capability, ioStreams cmdutil.
capdir, _ := system.GetCapabilityDir()
switch cap.Type {
case types.TypeTrait:
- return os.Remove(filepath.Join(capdir, "traits", cap.Name))
+ if err := os.Remove(filepath.Join(capdir, "traits", cap.Name)); err != nil {
+ return err
+ }
case types.TypeWorkload:
- return os.Remove(filepath.Join(capdir, "workloads", cap.Name))
+ if err := os.Remove(filepath.Join(capdir, "workloads", cap.Name)); err != nil {
+ return err
+ }
case types.TypeScope:
// TODO(wonderflow): add scope remove here.
}
- ioStreams.Infof("%s removed successfully", cap.Name)
+ ioStreams.Infof("Successfully uninstalled capability %s", cap.Name)
return nil
}
diff --git a/pkg/serverlib/trait_checker.go b/pkg/serverlib/trait_checker.go
deleted file mode 100644
index 7478c9054..000000000
--- a/pkg/serverlib/trait_checker.go
+++ /dev/null
@@ -1,217 +0,0 @@
-package serverlib
-
-import (
- "context"
- "encoding/json"
- "fmt"
-
- "github.com/oam-dev/kubevela/pkg/appfile/api"
-
- "github.com/oam-dev/kubevela/pkg/appfile"
-
- runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
- v12 "k8s.io/api/autoscaling/v1"
- v1 "k8s.io/api/core/v1"
- "k8s.io/api/networking/v1beta1"
- "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "sigs.k8s.io/controller-runtime/pkg/client"
-
- "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
- "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
- autoscalers "github.com/oam-dev/kubevela/pkg/controller/standard.oam.dev/v1alpha1/autoscaler"
- "github.com/oam-dev/kubevela/pkg/oam"
-)
-
-// CheckStatus defines the type of checking status
-type CheckStatus string
-
-const (
- // StatusChecking means in checking loop
- StatusChecking = "checking"
- // StatusDone means check has done
- StatusDone = "done"
-)
-
-// GetChecker will get Trait checker for 'vela status'
-func GetChecker(traitType string, c client.Client) Checker {
- switch traitType {
- case "route":
- return &RouteChecker{c: c}
- case "metrics":
- return &MetricChecker{c: c}
- case "autoscale":
- return &AutoscalerChecker{c: c}
- }
-
- return &DefaultChecker{c: c}
-}
-
-// Checker defines the interface of checker
-type Checker interface {
- Check(ctx context.Context, reference runtimev1alpha1.TypedReference, compName string, appConfig *v1alpha2.ApplicationConfiguration, app *api.Application) (CheckStatus, string, error)
-}
-
-// DefaultChecker defines the default checker
-type DefaultChecker struct {
- c client.Client
-}
-
-// Check default check object if exist and print the configs
-func (d *DefaultChecker) Check(ctx context.Context, reference runtimev1alpha1.TypedReference, compName string, appConfig *v1alpha2.ApplicationConfiguration, app *api.Application) (CheckStatus, string, error) {
- tr, err := GetUnstructured(ctx, d.c, appConfig.Namespace, reference)
- if err != nil {
- return StatusChecking, "", err
- }
- traitType, ok := tr.GetLabels()[oam.TraitTypeLabel]
- if !ok {
- message, err := GetStatusFromObject(tr)
- return StatusDone, message, err
- }
- traitData, err := appfile.GetTraitsByType(app, compName, traitType)
- if err != nil {
- return StatusDone, err.Error(), err
- }
- var message string
- for k, v := range traitData {
- message += fmt.Sprintf("%v=%v\n\t\t", k, v)
- }
- return StatusDone, message, err
-}
-
-// MetricChecker check for 'metrics' core trait
-type MetricChecker struct {
- c client.Client
-}
-
-// Check metrics
-func (d *MetricChecker) Check(ctx context.Context, reference runtimev1alpha1.TypedReference, _ string, appConfig *v1alpha2.ApplicationConfiguration, _ *api.Application) (CheckStatus, string, error) {
- metric := v1alpha1.MetricsTrait{}
- if err := d.c.Get(ctx, client.ObjectKey{Namespace: appConfig.Namespace, Name: reference.Name}, &metric); err != nil {
- return StatusChecking, "", err
- }
- condition := metric.Status.Conditions
- if len(condition) < 1 {
- return StatusChecking, "", nil
- }
- if condition[0].Status != v1.ConditionTrue {
- return StatusChecking, condition[0].Message, nil
- }
- if metric.Spec.ScrapeService.Enabled != nil && !*metric.Spec.ScrapeService.Enabled {
- return StatusDone, "Monitoring disabled", nil
- }
- var message = fmt.Sprintf("Monitoring port: %s, path: %s, format: %s, schema: %s.",
- metric.Status.Port.String(), metric.Spec.ScrapeService.Path,
- metric.Spec.ScrapeService.Format, metric.Spec.ScrapeService.Scheme)
- return StatusDone, message, nil
-}
-
-// RouteChecker check for 'route' core trait
-type RouteChecker struct {
- c client.Client
-}
-
-// Check understand route status
-func (d *RouteChecker) Check(ctx context.Context, reference runtimev1alpha1.TypedReference, _ string, appConfig *v1alpha2.ApplicationConfiguration, _ *api.Application) (CheckStatus, string, error) {
- route := v1alpha1.Route{}
- if err := d.c.Get(ctx, client.ObjectKey{Namespace: appConfig.Namespace, Name: reference.Name}, &route); err != nil {
- return StatusChecking, "", err
- }
- condition := route.Status.Conditions
- if len(condition) < 1 {
- return StatusChecking, "", nil
- }
- if condition[0].Status != v1.ConditionTrue {
- return StatusChecking, condition[0].Message, nil
- }
- var message string
- for _, ingress := range route.Status.Ingresses {
- var in v1beta1.Ingress
- if err := d.c.Get(ctx, client.ObjectKey{Namespace: appConfig.Namespace, Name: ingress.Name}, &in); err != nil {
- return StatusChecking, "", err
- }
- value := in.Status.LoadBalancer.Ingress
- if len(value) < 1 {
- return StatusChecking, "", fmt.Errorf("%s IP not assigned yet", in.Name)
- }
- var url string
- if len(in.Spec.TLS) >= 1 {
- url = "https://" + in.Spec.Rules[0].Host
- } else {
- url = "http://" + in.Spec.Rules[0].Host
- }
- addr := value[0].IP
- if value[0].Hostname != "" {
- addr = value[0].Hostname
- }
- message += fmt.Sprintf("\tVisiting URL: %s\tIP: %s\n", url, addr)
- }
- if len(route.Status.Ingresses) == 0 {
- message += fmt.Sprintf("Visiting by using 'vela port-forward %s --route'\n", appConfig.Name)
- }
- return StatusDone, message, nil
-}
-
-// AutoscalerChecker checks 'autoscale' trait
-type AutoscalerChecker struct {
- c client.Client
-}
-
-// Check should understand autoscale trait status
-func (d *AutoscalerChecker) Check(ctx context.Context, ref runtimev1alpha1.TypedReference, _ string, appConfig *v1alpha2.ApplicationConfiguration, _ *api.Application) (CheckStatus, string, error) {
- traitName := ref.Name
- var scaler v1alpha1.Autoscaler
- if err := d.c.Get(ctx, client.ObjectKey{Namespace: appConfig.Namespace, Name: traitName}, &scaler); err != nil {
- return StatusChecking, "", err
- }
- var scalerType string
- triggers := scaler.Spec.Triggers
- if len(triggers) >= 1 {
- scalerType = string(triggers[0].Type)
- }
-
- hpaName := "keda-hpa-" + traitName
- var hpa v12.HorizontalPodAutoscaler
- if err := d.c.Get(ctx, client.ObjectKey{Namespace: appConfig.Namespace, Name: hpaName}, &hpa); err != nil {
- return StatusChecking, "", err
- }
- message := fmt.Sprintf("type: %-8s", scalerType)
- if scalerType == string(autoscalers.CPUType) {
- // When attaching trait, and before the scaler trait works, `CurrentCPUUtilizationPercentage` is nil
- currentCPUUtilizationPercentage := hpa.Status.CurrentCPUUtilizationPercentage
- var zeroPercentage int32 = 0
- if currentCPUUtilizationPercentage == nil {
- currentCPUUtilizationPercentage = &zeroPercentage
- }
- message += fmt.Sprintf("cpu-utilization(target/current): %v%%/%v%%\t",
- *hpa.Spec.TargetCPUUtilizationPercentage, *currentCPUUtilizationPercentage)
- }
- message += fmt.Sprintf("replicas(min/max/current): %v/%v/%v", *hpa.Spec.MinReplicas, hpa.Spec.MaxReplicas,
- hpa.Status.CurrentReplicas)
- return StatusDone, message, nil
-}
-
-// GetUnstructured get object by GVK.
-func GetUnstructured(ctx context.Context, c client.Client, ns string, resourceRef runtimev1alpha1.TypedReference) (*unstructured.Unstructured, error) {
- resource := unstructured.Unstructured{}
- resource.SetGroupVersionKind(resourceRef.GroupVersionKind())
- if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: resourceRef.Name}, &resource); err != nil {
- return nil, err
- }
- return &resource, nil
-}
-
-// GetStatusFromObject get Unstructured object status
-func GetStatusFromObject(resource *unstructured.Unstructured) (string, error) {
- var message string
- statusData, foundStatus, _ := unstructured.NestedMap(resource.Object, "status")
- if foundStatus {
- statusJSON, err := json.Marshal(statusData)
- if err != nil {
- return "", err
- }
- message = string(statusJSON)
- } else {
- message = "status not found"
- }
- return fmt.Sprintf("%s status: %s", resource.GetName(), message), nil
-}
diff --git a/pkg/utils/apply/apply.go b/pkg/utils/apply/apply.go
index abb7d548b..2d68e32f4 100644
--- a/pkg/utils/apply/apply.go
+++ b/pkg/utils/apply/apply.go
@@ -11,13 +11,9 @@ import (
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
-)
-// An Object is a Kubernetes object.
-type Object interface {
- metav1.Object
- runtime.Object
-}
+ "github.com/oam-dev/kubevela/pkg/oam"
+)
// Applicator applies new state to an object or create it if not exist.
// It employes the same mechanism as `kubectl apply`, that is, for each resource being applied,
@@ -93,7 +89,7 @@ func (a *APIApplicator) Apply(ctx context.Context, desired runtime.Object, ao ..
// createOrGetExisting will create the object if it does not exist
// or get and return the existing object
func createOrGetExisting(ctx context.Context, c client.Client, desired runtime.Object, ao ...ApplyOption) (runtime.Object, error) {
- m, ok := desired.(Object)
+ m, ok := desired.(oam.Object)
if !ok {
return nil, errors.New("cannot access object metadata")
}
diff --git a/pkg/webhook/common/rollout/rollout_plan.go b/pkg/webhook/common/rollout/rollout_plan.go
index ccae056ce..8fe3620f3 100644
--- a/pkg/webhook/common/rollout/rollout_plan.go
+++ b/pkg/webhook/common/rollout/rollout_plan.go
@@ -1,25 +1,85 @@
package rollout
import (
+ "k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/validation/field"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
)
// DefaultRolloutPlan set the default values for a rollout plan
+// This is called by the mutation webhooks and before the validators
func DefaultRolloutPlan(rollout *v1alpha1.RolloutPlan) {
-
+ if rollout.TargetSize != nil && rollout.NumBatches != nil && rollout.RolloutBatches == nil {
+ // create the rollout batch based on the total size and num batches if it's not set
+ // leave it for the validator to reject if they are both set
+ numBatches := int(*rollout.NumBatches)
+ totalSize := int(*rollout.TargetSize)
+ // create the batch array
+ rollout.RolloutBatches = make([]v1alpha1.RolloutBatch, int(*rollout.NumBatches))
+ avg := intstr.FromInt(totalSize / numBatches)
+ total := 0
+ for i := 0; i < numBatches-1; i++ {
+ rollout.RolloutBatches[i].Replicas = avg
+ total += avg.IntValue()
+ }
+ // fill out the last batch
+ rollout.RolloutBatches[numBatches-1].Replicas = intstr.FromInt(totalSize - total)
+ }
}
// ValidateCreate validate the rollout plan
-func ValidateCreate(rollout *v1alpha1.RolloutPlan) field.ErrorList {
- // 1. The total number of replicas in the batches match the current target resource pod size
- // 2. The TargetSize and NumBatches are mutually exclusive to RolloutBatches
- return nil
+func ValidateCreate(rollout *v1alpha1.RolloutPlan, rootPath *field.Path) field.ErrorList {
+ var allErrs field.ErrorList
+ // The total number of num in the batches match the current target resource pod size
+
+ // The TargetSize and NumBatches are mutually exclusive to RolloutBatches
+ if rollout.NumBatches != nil && rollout.RolloutBatches != nil {
+ allErrs = append(allErrs, field.Duplicate(rootPath.Child("numBatches"), rollout.NumBatches))
+ }
+
+ // validate the webhooks
+ allErrs = append(allErrs, validateWebhook(rollout, rootPath)...)
+
+ return allErrs
+}
+
+func validateWebhook(rollout *v1alpha1.RolloutPlan, rootPath *field.Path) (allErrs field.ErrorList) {
+ // The webhooks in the rollout plan can only be initialize or finalize webhooks
+ if rollout.RolloutWebhooks != nil {
+ webhookPath := rootPath.Child("rolloutWebhooks")
+ for i, rw := range rollout.RolloutWebhooks {
+ if rw.Type != v1alpha1.InitializeRolloutHook && rw.Type != v1alpha1.FinalizeRolloutHook {
+ allErrs = append(allErrs, field.Invalid(webhookPath.Index(i),
+ rw.Type, "the rollout webhook type can only be initialize or finalize webhook"))
+ }
+ // TODO: check the URL/name uniqueness?
+ }
+ }
+
+ // The webhooks in the rollout batch can only be pre or post batch types
+ if rollout.RolloutBatches != nil {
+ batchesPath := rootPath.Child("rolloutBatches")
+ for i, rb := range rollout.RolloutBatches {
+ rolloutBatchPath := batchesPath.Index(i)
+ for j, brw := range rb.BatchRolloutWebhooks {
+ if brw.Type != v1alpha1.PostBatchRolloutHook && brw.Type != v1alpha1.PreBatchRolloutHook {
+ allErrs = append(allErrs, field.Invalid(rolloutBatchPath.Child("batchRolloutWebhooks").Index(j),
+ brw.Type, "the batch webhook type can only be pre or post batch webhook"))
+ }
+ // TODO: check the URL/name uniqueness?
+ }
+ }
+ }
+ return allErrs
}
// ValidateUpdate validate if one can change the rollout plan from the previous psec
-func ValidateUpdate(new *v1alpha1.RolloutPlan, prev *v1alpha1.RolloutPlan) field.ErrorList {
- // Only a few fields can change after a rollout plan is set
- return nil
+func ValidateUpdate(new *v1alpha1.RolloutPlan, prev *v1alpha1.RolloutPlan, rootPath *field.Path) field.ErrorList {
+ // makes sure the new rollout alone is valid
+ allErrs := ValidateCreate(new, rootPath)
+
+ // TODO: Enforce that only a few fields can change after a rollout plan is set
+
+ return allErrs
}
diff --git a/pkg/webhook/core.oam.dev/register.go b/pkg/webhook/core.oam.dev/register.go
index 0432c20eb..6b5a42919 100644
--- a/pkg/webhook/core.oam.dev/register.go
+++ b/pkg/webhook/core.oam.dev/register.go
@@ -7,6 +7,7 @@ import (
"github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration"
"github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/applicationdeployment"
"github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/component"
+ "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition"
)
// Register will be called in main and register all validation handlers
@@ -17,6 +18,9 @@ func Register(mgr manager.Manager) error {
if err := applicationconfiguration.RegisterValidatingHandler(mgr); err != nil {
return err
}
+ if err := traitdefinition.RegisterValidatingHandler(mgr); err != nil {
+ return err
+ }
applicationconfiguration.RegisterMutatingHandler(mgr)
applicationdeployment.RegisterMutatingHandler(mgr)
if err := component.RegisterMutatingHandler(mgr); err != nil {
diff --git a/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/helper.go b/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/helper.go
index 8c526c408..2455ff44e 100644
--- a/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/helper.go
+++ b/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/helper.go
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/pkg/errors"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -21,6 +22,7 @@ const (
errFmtUnmarshalWorkload = "cannot unmarshal workload of component %q"
errFmtUnmarshalTrait = "cannot unmarshal trait of component %q"
errFmtGetWorkloadDefinition = "cannot get workload definition of component %q"
+ errFmtCheckTrait = "failed checking trait of component %q"
)
// ValidatingAppConfig is used for validating ApplicationConfiguration
@@ -91,17 +93,22 @@ func (v *ValidatingAppConfig) PrepareForValidation(ctx context.Context, c client
tmpT := ValidatingTrait{}
tmpT.componentTrait = t
// get trait content from raw
- var tContentObject map[string]interface{}
- if err := json.Unmarshal(t.Trait.Raw, &tContentObject); err != nil {
+ tContent := unstructured.Unstructured{}
+ if err := json.Unmarshal(t.Trait.Raw, &tContent.Object); err != nil {
return errors.Wrapf(err, errFmtUnmarshalTrait, tmp.compName)
}
- tContent := unstructured.Unstructured{
- Object: tContentObject,
+
+ if err := checkTraitObj(&tContent); err != nil {
+ return errors.Wrapf(err, errFmtCheckTrait, tmp.compName)
}
+
// get trait definition
tDef, err := util.FetchTraitDefinition(ctx, c, dm, &tContent)
if err != nil {
- return errors.Wrapf(err, errFmtGetTraitDefinition, tmp.compName)
+ if !k8serrors.IsNotFound(err) {
+ return errors.Wrapf(err, errFmtGetTraitDefinition, tmp.compName)
+ }
+ tDef = util.GetDummyTraitDefinition(&tContent)
}
tmpT.traitContent = tContent
tmpT.traitDefinition = *tDef
@@ -112,6 +119,21 @@ func (v *ValidatingAppConfig) PrepareForValidation(ctx context.Context, c client
return nil
}
+// checkTraitObj checks trait whether it's muated correctly and has GVK.
+// Further validation on traits should provieded by validators but not here.
+func checkTraitObj(t *unstructured.Unstructured) error {
+ if t.Object[TraitTypeField] != nil {
+ return errors.New("the trait contains 'name' info that should be mutated to GVK")
+ }
+ if t.Object[TraitSpecField] != nil {
+ return errors.New("the trait contains 'properties' info that should be mutated to spec")
+ }
+ if len(t.GetAPIVersion()) == 0 || len(t.GetKind()) == 0 {
+ return errors.New("the trait data missing GVK")
+ }
+ return nil
+}
+
// checkParams will check whether exist parameter assigning value to workload name
func checkParams(cp []v1alpha2.ComponentParameter, cpv []v1alpha2.ComponentParameterValue) (bool, string) {
targetParams := make(map[string]bool)
diff --git a/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/helper_test.go b/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/helper_test.go
index 00b6f4730..4582a487c 100644
--- a/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/helper_test.go
+++ b/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/helper_test.go
@@ -6,11 +6,55 @@ import (
"github.com/stretchr/testify/assert"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
)
+func TestCheckTraitObj(t *testing.T) {
+ traitWithName := unstructured.Unstructured{
+ Object: make(map[string]interface{}),
+ }
+ unstructured.SetNestedField(traitWithName.Object, "test", TraitTypeField)
+
+ traitWithProperties := unstructured.Unstructured{
+ Object: make(map[string]interface{}),
+ }
+ unstructured.SetNestedField(traitWithProperties.Object, "test", TraitSpecField)
+
+ traitWithoutGVK := unstructured.Unstructured{}
+ traitWithoutGVK.SetAPIVersion("")
+ traitWithoutGVK.SetKind("")
+
+ tests := []struct {
+ caseName string
+ traitContent unstructured.Unstructured
+ want string
+ }{
+ {
+ caseName: "the trait contains 'name' info that should be mutated to GVK",
+ traitContent: traitWithName,
+ want: "the trait contains 'name' info",
+ },
+ {
+ caseName: "the trait contains 'properties' info that should be mutated to spec",
+ traitContent: traitWithProperties,
+ want: "the trait contains 'properties' info",
+ },
+ {
+ caseName: "the trait data missing GVK",
+ traitContent: traitWithoutGVK,
+ want: "the trait data missing GVK",
+ },
+ }
+
+ for _, tc := range tests {
+ result := checkTraitObj(&tc.traitContent)
+ assert.Contains(t, result.Error(), tc.want, fmt.Sprintf("Test case: %q", tc.caseName))
+ }
+}
+
func TestCheckParams(t *testing.T) {
wlNameValue := "wlName"
pName := "wlnameParam"
diff --git a/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/validating_handler.go b/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/validating_handler.go
index 5657d2326..8e91b3091 100644
--- a/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/validating_handler.go
+++ b/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/validating_handler.go
@@ -10,7 +10,6 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
- "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/klog"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
@@ -96,7 +95,7 @@ func (h *ValidatingHandler) Handle(ctx context.Context, req admission.Request) a
}
vAppConfig := &ValidatingAppConfig{}
if err := vAppConfig.PrepareForValidation(ctx, h.Client, h.Mapper, obj); err != nil {
- klog.Info("failed init appConfig before validation ", " name: ", obj.Name, " errMsg: ", err.Error())
+ klog.Info("failed preparing information before validation ", " name: ", obj.Name, " errMsg: ", err.Error())
return admission.Denied(err.Error())
}
for _, validator := range h.Validators {
@@ -110,35 +109,6 @@ func (h *ValidatingHandler) Handle(ctx context.Context, req admission.Request) a
return admission.ValidationResponse(true, "")
}
-// ValidateTraitObjectFn validates the ApplicationConfiguration on creation/update
-func ValidateTraitObjectFn(_ context.Context, v ValidatingAppConfig) []error {
- klog.Info("validate applicationConfiguration", "name", v.appConfig.Name)
- var allErrs field.ErrorList
- for cidx, comp := range v.validatingComps {
- for idx, tr := range comp.validatingTraits {
- fldPath := field.NewPath("spec").Child("components").Index(cidx).Child("traits").Index(idx).Child("trait")
- content := tr.traitContent.Object
- if content[TraitTypeField] != nil {
- allErrs = append(allErrs, field.Invalid(fldPath, string(tr.componentTrait.Trait.Raw),
- "the trait contains 'name' info that should be mutated to GVK"))
- }
- if content[TraitSpecField] != nil {
- allErrs = append(allErrs, field.Invalid(fldPath, string(tr.componentTrait.Trait.Raw),
- "the trait contains 'properties' info that should be mutated to spec"))
- }
- if len(tr.traitContent.GetAPIVersion()) == 0 || len(tr.traitContent.GetKind()) == 0 {
- allErrs = append(allErrs, field.Invalid(fldPath, content,
- fmt.Sprintf("the trait data missing GVK, api = %s, kind = %s,",
- tr.traitContent.GetAPIVersion(), tr.traitContent.GetKind())))
- }
- }
- }
- if len(allErrs) > 0 {
- return allErrs.ToAggregate().Errors()
- }
- return nil
-}
-
// ValidateRevisionNameFn validates revisionName and componentName are assigned both.
func ValidateRevisionNameFn(_ context.Context, v ValidatingAppConfig) []error {
klog.Info("validate revisionName in applicationConfiguration", "name", v.appConfig.Name)
@@ -313,7 +283,6 @@ func RegisterValidatingHandler(mgr manager.Manager) error {
server.Register("/validating-core-oam-dev-v1alpha2-applicationconfigurations", &webhook.Admission{Handler: &ValidatingHandler{
Mapper: mapper,
Validators: []AppConfigValidator{
- AppConfigValidateFunc(ValidateTraitObjectFn),
AppConfigValidateFunc(ValidateRevisionNameFn),
AppConfigValidateFunc(ValidateWorkloadNameForVersioningFn),
AppConfigValidateFunc(ValidateTraitAppliableToWorkloadFn),
diff --git a/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/validating_handler_test.go b/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/validating_handler_test.go
index 88fe3ecc3..1eeaaaf1a 100644
--- a/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/validating_handler_test.go
+++ b/pkg/webhook/core.oam.dev/v1alpha2/applicationconfiguration/validating_handler_test.go
@@ -11,7 +11,6 @@ import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/intstr"
)
@@ -75,61 +74,6 @@ func TestValidateRevisionNameFn(t *testing.T) {
}
}
-func TestValidateTraitObjectFn(t *testing.T) {
- traitWithName := unstructured.Unstructured{
- Object: make(map[string]interface{}),
- }
- unstructured.SetNestedField(traitWithName.Object, "test", TraitTypeField)
-
- traitWithProperties := unstructured.Unstructured{
- Object: make(map[string]interface{}),
- }
- unstructured.SetNestedField(traitWithProperties.Object, "test", TraitSpecField)
-
- traitWithoutGVK := unstructured.Unstructured{}
- traitWithoutGVK.SetAPIVersion("")
- traitWithoutGVK.SetKind("")
-
- tests := []struct {
- caseName string
- traitContent unstructured.Unstructured
- want string
- }{
- {
- caseName: "the trait contains 'name' info that should be mutated to GVK",
- traitContent: traitWithName,
- want: "the trait contains 'name' info",
- },
- {
- caseName: "the trait contains 'properties' info that should be mutated to spec",
- traitContent: traitWithProperties,
- want: "the trait contains 'properties' info",
- },
- {
- caseName: "the trait data missing GVK",
- traitContent: traitWithoutGVK,
- want: "the trait data missing GVK",
- },
- }
-
- for _, tc := range tests {
- vAppConfig := ValidatingAppConfig{
- validatingComps: []ValidatingComponent{
- {
- validatingTraits: []ValidatingTrait{
- {
- traitContent: tc.traitContent,
- },
- },
- },
- },
- }
- allErrs := ValidateTraitObjectFn(ctx, vAppConfig)
- result := utilerrors.NewAggregate(allErrs).Error()
- assert.Contains(t, result, tc.want, fmt.Sprintf("Test case: %q", tc.caseName))
- }
-}
-
func TestValidateWorkloadNameForVersioningFn(t *testing.T) {
workloadName := "wl-name"
wlWithName := unstructured.Unstructured{}
diff --git a/pkg/webhook/core.oam.dev/v1alpha2/applicationdeployment/validation.go b/pkg/webhook/core.oam.dev/v1alpha2/applicationdeployment/validation.go
index 14079673d..2ca9f7ff1 100644
--- a/pkg/webhook/core.oam.dev/v1alpha2/applicationdeployment/validation.go
+++ b/pkg/webhook/core.oam.dev/v1alpha2/applicationdeployment/validation.go
@@ -53,7 +53,7 @@ func (h *ValidatingHandler) ValidateCreate(appDeploy *v1alpha2.ApplicationDeploy
fldPath.Child("componentList"))...)
// validate the rollout plan spec
- allErrs = append(allErrs, rollout.ValidateCreate(&appDeploy.Spec.RolloutPlan)...)
+ allErrs = append(allErrs, rollout.ValidateCreate(&appDeploy.Spec.RolloutPlan, fldPath.Child("rolloutPlan"))...)
return allErrs
}
@@ -113,5 +113,6 @@ func (h *ValidatingHandler) ValidateUpdate(new, old *v1alpha2.ApplicationDeploym
if len(errList) > 0 {
return errList
}
- return rollout.ValidateUpdate(&new.Spec.RolloutPlan, &old.Spec.RolloutPlan)
+ fldPath := field.NewPath("spec").Child("rolloutPlan")
+ return rollout.ValidateUpdate(&new.Spec.RolloutPlan, &old.Spec.RolloutPlan, fldPath)
}
diff --git a/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler.go b/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler.go
new file mode 100644
index 000000000..8469d1cce
--- /dev/null
+++ b/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler.go
@@ -0,0 +1,135 @@
+package traitdefinition
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/oam-dev/kubevela/pkg/oam/util"
+
+ admissionv1beta1 "k8s.io/api/admission/v1beta1"
+ "k8s.io/klog"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/manager"
+ "sigs.k8s.io/controller-runtime/pkg/runtime/inject"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+ "github.com/pkg/errors"
+
+ "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
+ "github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
+)
+
+const (
+ errValidateDefRef = "error occurs when validating definition reference"
+
+ failInfoDefRefOmitted = "if definition reference is omitted, patch or output with GVK is required"
+)
+
+var traitDefGVR = v1alpha2.SchemeGroupVersion.WithResource("traitdefinitions")
+
+// ValidatingHandler handles validation of trait definition
+type ValidatingHandler struct {
+ Client client.Client
+ Mapper discoverymapper.DiscoveryMapper
+
+ // Decoder decodes object
+ Decoder *admission.Decoder
+ // Validators validate objects
+ Validators []TraitDefValidator
+}
+
+// TraitDefValidator validate trait definition
+type TraitDefValidator interface {
+ Validate(context.Context, v1alpha2.TraitDefinition) error
+}
+
+// TraitDefValidatorFn implements TraitDefValidator
+type TraitDefValidatorFn func(context.Context, v1alpha2.TraitDefinition) error
+
+// Validate implements TraitDefValidator method
+func (fn TraitDefValidatorFn) Validate(ctx context.Context, td v1alpha2.TraitDefinition) error {
+ return fn(ctx, td)
+}
+
+var _ admission.Handler = &ValidatingHandler{}
+
+// Handle validate trait definition
+func (h *ValidatingHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
+ obj := &v1alpha2.TraitDefinition{}
+ if req.Resource.String() != traitDefGVR.String() {
+ return admission.Errored(http.StatusBadRequest, fmt.Errorf("expect resource to be %s", traitDefGVR))
+ }
+
+ if req.Operation == admissionv1beta1.Create || req.Operation == admissionv1beta1.Update {
+ err := h.Decoder.Decode(req, obj)
+ if err != nil {
+ return admission.Errored(http.StatusBadRequest, err)
+ }
+ klog.Info("validating ", " name: ", obj.Name, " operation: ", string(req.Operation))
+ for _, validator := range h.Validators {
+ if err := validator.Validate(ctx, *obj); err != nil {
+ klog.Info("validation failed ", " name: ", obj.Name, " errMsgi: ", err.Error())
+ return admission.Denied(err.Error())
+ }
+ }
+ klog.Info("validation passed ", " name: ", obj.Name, " operation: ", string(req.Operation))
+ }
+ return admission.ValidationResponse(true, "")
+}
+
+var _ inject.Client = &ValidatingHandler{}
+
+// InjectClient injects the client into the ValidatingHandler
+func (h *ValidatingHandler) InjectClient(c client.Client) error {
+ h.Client = c
+ return nil
+}
+
+var _ admission.DecoderInjector = &ValidatingHandler{}
+
+// InjectDecoder injects the decoder into the ValidatingHandler
+func (h *ValidatingHandler) InjectDecoder(d *admission.Decoder) error {
+ h.Decoder = d
+ return nil
+}
+
+// RegisterValidatingHandler will register TraitDefinition validation to webhook
+func RegisterValidatingHandler(mgr manager.Manager) error {
+ server := mgr.GetWebhookServer()
+ mapper, err := discoverymapper.New(mgr.GetConfig())
+ if err != nil {
+ return err
+ }
+ server.Register("/validating-core-oam-dev-v1alpha2-traitdefinitions", &webhook.Admission{Handler: &ValidatingHandler{
+ Mapper: mapper,
+ Validators: []TraitDefValidator{
+ TraitDefValidatorFn(ValidateDefinitionReference),
+ // add more validators here
+ },
+ }})
+ return nil
+}
+
+// ValidateDefinitionReference validates whether the trait definition is valid if
+// its `.spec.reference` field is unset.
+// It's valid if
+// it has at least one output, and all outputs must have GVK
+// or it has no output but has a patch
+// or it has a patch and outputs, and all outputs must have GVK
+// TODO(roywang) currently we only validate whether it contains CUE template.
+// Further validation, e.g., output with GVK, valid patch, etc, remains to be done.
+func ValidateDefinitionReference(_ context.Context, td v1alpha2.TraitDefinition) error {
+ if len(td.Spec.Reference.Name) > 0 {
+ return nil
+ }
+ tmp, err := util.NewTemplate(td.Spec.Template, td.Spec.Status, td.Spec.Extension)
+ if err != nil {
+ return errors.Wrap(err, errValidateDefRef)
+ }
+ if len(tmp.TemplateStr) == 0 {
+ return errors.New(failInfoDefRefOmitted)
+ }
+ return nil
+}
diff --git a/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler_test.go b/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler_test.go
new file mode 100644
index 000000000..15b4565a6
--- /dev/null
+++ b/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler_test.go
@@ -0,0 +1,123 @@
+package traitdefinition
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "github.com/pkg/errors"
+
+ admissionv1beta1 "k8s.io/api/admission/v1beta1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+ "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
+)
+
+var handler ValidatingHandler
+var req admission.Request
+var reqResource metav1.GroupVersionResource
+var decoder *admission.Decoder
+var td v1alpha2.TraitDefinition
+var tdRaw []byte
+var scheme = runtime.NewScheme()
+
+func TestTraitdefinition(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Traitdefinition Suite")
+}
+
+var _ = BeforeSuite(func(done Done) {
+ td = v1alpha2.TraitDefinition{}
+ td.SetGroupVersionKind(v1alpha2.TraitDefinitionGroupVersionKind)
+ tdRaw, _ = json.Marshal(td)
+
+ var err error
+ decoder, err = admission.NewDecoder(scheme)
+ Expect(err).Should(BeNil())
+
+ close(done)
+})
+
+var _ = Describe("Test TraitDefinition validating handler", func() {
+ BeforeEach(func() {
+ reqResource = metav1.GroupVersionResource{
+ Group: v1alpha2.Group,
+ Version: v1alpha2.Version,
+ Resource: "traitdefinitions"}
+ handler = ValidatingHandler{}
+ handler.InjectDecoder(decoder)
+ })
+
+ It("Test wrong resource of admission request", func() {
+ wrongReqResource := metav1.GroupVersionResource{
+ Group: v1alpha2.Group,
+ Version: v1alpha2.Version,
+ Resource: "foos"}
+ req = admission.Request{
+ AdmissionRequest: admissionv1beta1.AdmissionRequest{
+ Operation: admissionv1beta1.Create,
+ Resource: wrongReqResource,
+ Object: runtime.RawExtension{Raw: []byte("")},
+ },
+ }
+ resp := handler.Handle(context.TODO(), req)
+ Expect(resp.Allowed).Should(BeFalse())
+ })
+
+ It("Test bad admission request", func() {
+ req = admission.Request{
+ AdmissionRequest: admissionv1beta1.AdmissionRequest{
+ Operation: admissionv1beta1.Create,
+ Resource: reqResource,
+ Object: runtime.RawExtension{Raw: []byte("bad request")},
+ },
+ }
+ resp := handler.Handle(context.TODO(), req)
+ Expect(resp.Allowed).Should(BeFalse())
+ })
+
+ Context("Test create/update operation admission request", func() {
+ var mockValidator TraitDefValidatorFn
+ It("Test validation passed", func() {
+ // mock a validator that always validates successfully
+ mockValidator = func(_ context.Context, _ v1alpha2.TraitDefinition) error {
+ return nil
+ }
+ handler.Validators = []TraitDefValidator{
+ TraitDefValidatorFn(mockValidator),
+ }
+ req = admission.Request{
+ AdmissionRequest: admissionv1beta1.AdmissionRequest{
+ Operation: admissionv1beta1.Create,
+ Resource: reqResource,
+ Object: runtime.RawExtension{Raw: tdRaw},
+ },
+ }
+ resp := handler.Handle(context.TODO(), req)
+ Expect(resp.Allowed).Should(BeTrue())
+ })
+ It("Test validation failed", func() {
+ // mock a validator that always failed
+ mockValidator = func(_ context.Context, _ v1alpha2.TraitDefinition) error {
+ return errors.New("mock validator error")
+ }
+ handler.Validators = []TraitDefValidator{
+ TraitDefValidatorFn(mockValidator),
+ }
+ req = admission.Request{
+ AdmissionRequest: admissionv1beta1.AdmissionRequest{
+ Operation: admissionv1beta1.Create,
+ Resource: reqResource,
+ Object: runtime.RawExtension{Raw: tdRaw},
+ },
+ }
+ resp := handler.Handle(context.TODO(), req)
+ Expect(resp.Allowed).Should(BeFalse())
+ Expect(resp.Result.Reason).Should(Equal(metav1.StatusReason("mock validator error")))
+ })
+ })
+})
diff --git a/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validator_test.go b/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validator_test.go
new file mode 100644
index 000000000..16c0f023c
--- /dev/null
+++ b/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validator_test.go
@@ -0,0 +1,69 @@
+package traitdefinition
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/crossplane/crossplane-runtime/pkg/test"
+ "github.com/google/go-cmp/cmp"
+ "github.com/pkg/errors"
+
+ "github.com/oam-dev/kubevela/pkg/oam/util"
+)
+
+func TestValidateDefinitionReference(t *testing.T) {
+ cases := map[string]struct {
+ reason string
+ template string
+ want error
+ }{
+ "NoExtension": {
+ reason: "An error should be returned if extension is omitted",
+ template: "",
+ want: errors.New(failInfoDefRefOmitted),
+ },
+ "HaveExtentsion_NoTemplate": {
+ reason: "An error should be returned if extension template is omitted",
+ template: `
+ extension:
+ notemplate: |-
+ fakefield: fakefieldvalue`,
+ want: errors.New(failInfoDefRefOmitted),
+ },
+ "HaveExtension_HaveTemplate": {
+ reason: "No error should be returned if have CUE template",
+ template: `
+ extension:
+ template: |-
+ patch: {
+ spec: replicas: parameter.replicas
+ }`,
+ want: nil,
+ },
+ }
+
+ for caseName, tc := range cases {
+ t.Run(caseName, func(t *testing.T) {
+ tdStr := traitDefStringWithTemplate(tc.template)
+ td, err := util.UnMarshalStringToTraitDefinition(tdStr)
+ if err != nil {
+ t.Fatal("error occurs in generating TraitDefinition string", err.Error())
+ }
+ err = ValidateDefinitionReference(context.Background(), *td)
+ if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nValidateDefinitionReference: -want , +got \n%s\n", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func traitDefStringWithTemplate(t string) string {
+ return fmt.Sprintf(`
+apiVersion: core.oam.dev/v1alpha2
+kind: TraitDefinition
+metadata:
+ name: scaler
+spec:
+%s`, t)
+}