Merge pull request #1034 from oam-dev/master

merge master to release-0.3 align with v0.3.3
This commit is contained in:
Jianbo Sun
2021-02-07 17:49:09 +08:00
committed by GitHub
132 changed files with 4757 additions and 2234 deletions
+25 -6
View File
@@ -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
run: ./ossutil --config-file .ossutilconfig sync $LOCAL_OSS_DIRECTORY oss://$BUCKET/core -f
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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/
+1
View File
@@ -6,6 +6,7 @@
[![Releases](https://img.shields.io/github/release/oam-dev/kubevela/all.svg?style=flat-square)](https://github.com/oam-dev/kubevela/releases)
[![TODOs](https://img.shields.io/endpoint?url=https://api.tickgit.com/badge?repo=github.com/oam-dev/kubevela)](https://www.tickgit.com/browse?repo=github.com/oam-dev/kubevela)
[![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Foam_dev)](https://twitter.com/oam_dev)
[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/kubevela)](https://artifacthub.io/packages/search?repo=kubevela)
![alt](docs/resources/KubeVela-03.png)
+16 -3
View File
@@ -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
@@ -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
+45
View File
@@ -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"`
@@ -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)
+1 -1
View File
@@ -12,7 +12,7 @@
//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../hack/boilerplate.go.txt paths=./... crd:trivialVersions=true output:artifacts:config=../legacy/charts/vela-core-legacy/crds
//go:generate go run ../legacy/convert/main.go ../legacy/charts/vela-core-legacy/crds
//go:generate go run ../hack/crd/update.go ../charts/vela-core/crds/
//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
@@ -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
@@ -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))
}
}
@@ -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
-16
View File
@@ -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
+1 -1
View File
@@ -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.
#
@@ -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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: []
@@ -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
@@ -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
@@ -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
}
},
]
}
}]
}
}
@@ -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
}
@@ -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]
}
@@ -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
}
@@ -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]
}
@@ -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 }}
+22
View File
@@ -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:
+2
View File
@@ -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: ""
+23 -5
View File
@@ -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)
+21
View File
@@ -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
@@ -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
}
},
]
}
}]
}
}
+39 -47
View File
@@ -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
```
+6
View File
@@ -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',
+4 -1
View File
@@ -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"
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+3 -1
View File
@@ -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 () => {
<div style={{ marginBottom: '10px' }}>
<Space>
<Button type="primary" icon={<PlusOutlined />}>
Create
<ReactLink to="/applications/create"> Create</ReactLink>
</Button>
</Space>
</div>
@@ -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) => (
<Menu.Item
key={i.name}
icon={<UserOutlined />}
onClick={() => handleMenuClick('workload_type', i.name)}
>
{i.name}
</Menu.Item>
));
const traitMenuList = traitsList?.map((i) => (
<Menu.Item
key={i.name}
icon={<UserOutlined />}
onClick={() => handleMenuClick('trait', i.name)}
>
{i.name}
</Menu.Item>
));
const workloadsMenu = <Menu>{workloadMenuList}</Menu>;
const traitsMenu = <Menu>{traitMenuList}</Menu>;
// 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 (
<div style={{ maxWidth: 600 }}>
<Row>
<Col span="4">Application</Col>
<Col span="20" />
</Row>
<Row>
<Col span="4">Name:</Col>
<Col span="8">
<Input placeholder="Basic usage" />
</Col>
<Col span="12" />
</Row>
<Row>
<Col span="24">
<Divider />
</Col>
</Row>
<Row>
<Col span="4">Services</Col>
<Col span="20" />
</Row>
<Row>
<Col span="4">Name:</Col>
<Col span="8">
<Input placeholder="Basic usage" />
</Col>
<Col span="12" />
</Row>
<Row>
<Col span="4">Type:</Col>
<Col span="20">
<Dropdown overlay={workloadsMenu}>
<a className="ant-dropdown-link">
Select <DownOutlined />
</a>
</Dropdown>
</Col>
</Row>
<Row>
<Col span="4">Settings:</Col>
<Col span="20">
<FormRender
schema={workloadSchema}
formData={formData}
onChange={setData}
onValidate={setValid}
displayType="column"
/>
</Col>
</Row>
<Row>
<Col span="24">
<Divider />
</Col>
</Row>
<Row>
<Col span="4">Traits</Col>
<Col span="20" />
</Row>
<Row>
<Col span="4">Type:</Col>
<Col span="20">
<Dropdown overlay={traitsMenu}>
<a className="ant-dropdown-link" onClick={(e) => e.preventDefault()}>
Select <DownOutlined />
</a>
</Dropdown>
</Col>
</Row>
<Row>
<Col span="4">Properties:</Col>
<Col span="20">
<FormRender
schema={traitSchema}
formData={formData}
onChange={setData}
onValidate={setValid}
displayType="column"
/>
</Col>
</Row>
<Row>
<Col span="8" />
<Col span="8">
<Button onClick={onSubmit} type="primary">
Submit
</Button>
</Col>
<Col span="8" />
</Row>
</div>
);
};
+21
View File
@@ -0,0 +1,21 @@
import { request } from 'umi';
/*
* workload type list: get /api/workloads/
*/
export async function getWorkloads(): Promise<API.VelaResponse<API.Workloads[]>> {
return request('/api/workloads');
}
/*
* trait list: get /api/traits/
*/
export async function getTraits(): Promise<API.VelaResponse<API.Traits[]>> {
return request('/api/traits');
}
export async function getCapabilityOpenAPISchema(
capabilityName: string,
): Promise<API.VelaResponse<string>> {
return request(`/api/definitions/${capabilityName}`, { method: 'get' });
}
-10
View File
@@ -1,10 +0,0 @@
import { request } from 'umi';
const BASE_PATH = '/api/traits';
/*
* trait 列表: get /api/traits/
*/
export async function getTraits(): Promise<API.VelaResponse<API.Traits[]>> {
return request(BASE_PATH);
}
-10
View File
@@ -1,10 +0,0 @@
import { request } from 'umi';
const BASE_PATH = '/api/workloads';
/*
* workload 列表: get /api/workloads/
*/
export async function getWorkloads(): Promise<API.VelaResponse<API.Workloads[]>> {
return request(BASE_PATH);
}
+8 -8
View File
@@ -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/<catalog>` 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
![alt](../../docs/resources/catalog-workflow.jpg)
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.
+25
View File
@@ -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
```
+5 -2
View File
@@ -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`.
+1 -2
View File
@@ -4,8 +4,7 @@
- [Concepts and Glossaries](/en/concepts.md)
- Platform Team Guide
- [Overview](/en/platform-engineers/overview.md)
<!-- - [Define Environments](TBD) -->
- [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)
@@ -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
+1 -1
View File
@@ -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.
+77 -2
View File
@@ -1,3 +1,78 @@
# KubeVela for Platform Builders
# What is KubeVela?
TBD: this documentation is still work in progress.
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.
![alt](../../resources/kubevela-runtime.png)
### 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.
+1 -2
View File
@@ -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: <your IP address>
```
**In [kind cluster setup](./install.md#kind)**, you can visit the service via localhost. In other setups, replace localhost with ingress address accordingly.
+4 -2
View File
@@ -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
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

+43 -6
View File
@@ -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)
+1 -1
View File
@@ -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
+7 -7
View File
@@ -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=
+23
View File
@@ -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
+40 -44
View File
@@ -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
}
+11 -2
View File
@@ -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: |
@@ -11,5 +11,4 @@ spec:
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
extension:
template: |-
template: |
+1 -2
View File
@@ -7,5 +7,4 @@ metadata:
spec:
definitionRef:
name: jobs.batch
extension:
template: |
template: |
@@ -8,5 +8,4 @@ metadata:
spec:
definitionRef:
name: deployments.apps
extension:
template: |
template: |
+1 -2
View File
@@ -7,5 +7,4 @@ metadata:
spec:
definitionRef:
name: deployments.apps
extension:
template: |
template: |
+1 -1
View File
@@ -16,7 +16,7 @@ echo "# Code generated by KubeVela templates. DO NOT EDIT." >> tmpC
for filename in `ls cue`; do
cat "cue/${filename}" > tmp
echo "" >> tmp
sed -i.bak 's/^/ /' tmp
sed -i.bak 's/^/ /' tmp
nameonly="${filename%.*}"
+38
View File
@@ -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
```
+21
View File
@@ -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
@@ -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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: []
@@ -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
@@ -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
+1 -1
View File
@@ -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() {
+6 -1
View File
@@ -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
}
+52 -28
View File
@@ -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
}
+8 -4
View File
@@ -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())
})
+1 -1
View File
@@ -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 {
+1
View File
@@ -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 {
+2 -2
View File
@@ -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),
+5 -3
View File
@@ -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)
+1 -2
View File
@@ -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,
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -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",
+74 -149
View File
@@ -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
+3 -2
View File
@@ -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
@@ -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)
}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
@@ -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())
@@ -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 {
@@ -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
}
}
@@ -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"
@@ -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{}
}
}
@@ -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())
})
})
})

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