Feat: support url in ref-objects (#4240)

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
Somefive 2022-06-24 19:34:51 +08:00 committed by GitHub
parent c4e1f39d28
commit 92fa67cd69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 170 additions and 7 deletions

View File

@ -25,6 +25,8 @@ const (
type RefObjectsComponentSpec struct {
// Objects the referrers to the Kubernetes objects
Objects []ObjectReferrer `json:"objects,omitempty"`
// URLs are the links that stores the referred objects
URLs []string `json:"urls,omitempty"`
}
// ObjectReferrer selects Kubernetes objects

View File

@ -595,6 +595,11 @@ func (in *RefObjectsComponentSpec) DeepCopyInto(out *RefObjectsComponentSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.URLs != nil {
in, out := &in.URLs, &out.URLs
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RefObjectsComponentSpec.

View File

@ -22,7 +22,12 @@ spec:
}
...
}
output: parameter.objects[0]
output: {
if len(parameter.objects) > 0 {
parameter.objects[0]
}
...
}
outputs: {
for i, v in parameter.objects {
if i > 0 {

View File

@ -22,7 +22,12 @@ spec:
}
...
}
output: parameter.objects[0]
output: {
if len(parameter.objects) > 0 {
parameter.objects[0]
}
...
}
outputs: {
for i, v in parameter.objects {
if i > 0 {

View File

@ -51,6 +51,7 @@ import (
"github.com/oam-dev/kubevela/pkg/cue/process"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
utilscommon "github.com/oam-dev/kubevela/pkg/utils/common"
)
// constant error information
@ -931,6 +932,13 @@ func (af *Appfile) LoadDynamicComponent(ctx context.Context, cli client.Client,
}
uns = component.AppendUnstructuredObjects(uns, objs...)
}
// nolint
for _, url := range spec.URLs {
objs := utilscommon.FilterObjectsByCondition(af.ReferredObjects, func(obj *unstructured.Unstructured) bool {
return obj.GetAnnotations() != nil && obj.GetAnnotations()[oam.AnnotationResourceURL] == url
})
uns = component.AppendUnstructuredObjects(uns, objs...)
}
refObjs, err := component.ConvertUnstructuredsToReferredObjects(uns)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal referred object")

View File

@ -45,6 +45,7 @@ import (
"github.com/oam-dev/kubevela/pkg/oam/util"
policypkg "github.com/oam-dev/kubevela/pkg/policy"
"github.com/oam-dev/kubevela/pkg/utils"
utilscommon "github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/workflow/step"
wftypes "github.com/oam-dev/kubevela/pkg/workflow/types"
)
@ -349,6 +350,16 @@ func (p *Parser) parseReferredObjects(ctx context.Context, af *Appfile) error {
}
af.ReferredObjects = component.AppendUnstructuredObjects(af.ReferredObjects, objs...)
}
for _, url := range spec.URLs {
objs, err := utilscommon.HTTPGetKubernetesObjects(ctx, url)
if err != nil {
return fmt.Errorf("failed to load Kubernetes objects from url %s: %w", url, err)
}
for _, obj := range objs {
util.AddAnnotations(obj, map[string]string{oam.AnnotationResourceURL: url})
}
af.ReferredObjects = component.AppendUnstructuredObjects(af.ReferredObjects, objs...)
}
}
sort.Slice(af.ReferredObjects, func(i, j int) bool {
a, b := af.ReferredObjects[i], af.ReferredObjects[j]

View File

@ -37,6 +37,8 @@ import (
velaclient "github.com/oam-dev/kubevela/pkg/client"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
utilscommon "github.com/oam-dev/kubevela/pkg/utils/common"
)
const (
@ -168,6 +170,9 @@ func SelectRefObjectsForDispatch(ctx context.Context, cli client.Client, appNs s
// ReferredObjectsDelegatingClient delegate client get/list function by retrieving ref-objects from existing objects
func ReferredObjectsDelegatingClient(cli client.Client, objs []*unstructured.Unstructured) client.Client {
objs = utilscommon.FilterObjectsByCondition(objs, func(obj *unstructured.Unstructured) bool {
return obj.GetAnnotations() == nil || obj.GetAnnotations()[oam.AnnotationResourceURL] == ""
})
return velaclient.DelegatingHandlerClient{
Client: cli,
Getter: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {

View File

@ -216,4 +216,7 @@ const (
// AnnotationAppSharedBy records who share the application
AnnotationAppSharedBy = "app.oam.dev/shared-by"
// AnnotationResourceURL records the source url of the Kubernetes object
AnnotationResourceURL = "app.oam.dev/resource-url"
)

View File

@ -40,6 +40,7 @@ import (
kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
errors2 "github.com/pkg/errors"
certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1"
yamlv3 "gopkg.in/yaml.v3"
istioclientv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
v1 "k8s.io/api/core/v1"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@ -146,8 +147,8 @@ func GetClient() (client.Client, error) {
return nil, errors.New("client not set, call SetGlobalClient first")
}
// HTTPGetWithOption use HTTP option and default client to send get request
func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byte, error) {
// HTTPGetResponse use HTTP option and default client to send request and get raw response
func HTTPGetResponse(ctx context.Context, url string, opts *HTTPOption) (*http.Response, error) {
// Change NewRequest to NewRequestWithContext and pass context it
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
@ -156,7 +157,12 @@ func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byt
if opts != nil && len(opts.Username) != 0 && len(opts.Password) != 0 {
req.SetBasicAuth(opts.Username, opts.Password)
}
resp, err := http.DefaultClient.Do(req)
return http.DefaultClient.Do(req)
}
// HTTPGetWithOption use HTTP option and default client to send get request
func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byte, error) {
resp, err := HTTPGetResponse(ctx, url, opts)
if err != nil {
return nil, err
}
@ -165,6 +171,29 @@ func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byt
return io.ReadAll(resp.Body)
}
// HTTPGetKubernetesObjects use HTTP requests to load resources from remote url
func HTTPGetKubernetesObjects(ctx context.Context, url string) ([]*unstructured.Unstructured, error) {
resp, err := HTTPGetResponse(ctx, url, nil)
if err != nil {
return nil, err
}
//nolint:errcheck
defer resp.Body.Close()
decoder := yamlv3.NewDecoder(resp.Body)
var uns []*unstructured.Unstructured
for {
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := decoder.Decode(obj.Object); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("failed to decode object: %w", err)
}
uns = append(uns, obj)
}
return uns, nil
}
// GetCUEParameterValue converts definitions to cue format
func GetCUEParameterValue(cueStr string, pd *packages.PackageDiscover) (cue.Value, error) {
var template *cue.Instance
@ -566,3 +595,13 @@ func NewK8sClient() (client.Client, error) {
}
return k8sClient, nil
}
// FilterObjectsByCondition filter object slices by condition function
func FilterObjectsByCondition(objs []*unstructured.Unstructured, filter func(unstructured2 *unstructured.Unstructured) bool) (outs []*unstructured.Unstructured) {
for _, obj := range objs {
if filter(obj) {
outs = append(outs, obj)
}
}
return
}

View File

@ -508,3 +508,15 @@ func TestRemoveEmptyString(t *testing.T) {
assert.NotEmpty(t, s)
}
}
func TestHTTPGetKubernetesObjects(t *testing.T) {
_, err := HTTPGetKubernetesObjects(context.Background(), "invalid-url")
assert.NotNil(t, err)
uns, err := HTTPGetKubernetesObjects(context.Background(), "https://gist.githubusercontent.com/Somefive/b189219a9222eaa70b8908cf4379402b/raw/920e83b1a2d56b584f9d8c7a97810a505a0bbaad/example-busybox-resources.yaml")
assert.NoError(t, err)
assert.Equal(t, 2, len(uns))
assert.Equal(t, "busybox", uns[0].GetName())
assert.Equal(t, "Deployment", uns[0].GetKind())
assert.Equal(t, "busybox", uns[1].GetName())
assert.Equal(t, "ConfigMap", uns[1].GetKind())
}

View File

@ -156,7 +156,23 @@ func (g *DeployWorkflowStepGenerator) Generate(app *v1beta1.Application, existin
}),
})
}
return
if len(topologies) == 0 {
containsRefObjects := false
for _, comp := range app.Spec.Components {
if comp.Type == v1alpha1.RefObjectsComponentType {
containsRefObjects = true
break
}
}
if containsRefObjects {
steps = append(steps, v1beta1.WorkflowStep{
Name: "deploy",
Type: "deploy",
Properties: util.Object2RawExtension(map[string]interface{}{"policies": append([]string{}, overrides...)}),
})
}
}
return steps, nil
}
// DeployPreApproveWorkflowStepGenerator generate suspend workflow steps before all deploy steps

View File

@ -166,6 +166,22 @@ func TestWorkflowStepGenerator(t *testing.T) {
Properties: &runtime.RawExtension{Raw: []byte(`{"policies":["example-override-policy-1","example-override-policy-2","example-topology-policy-2"]}`)},
}},
},
"deploy-with-ref-without-po-workflow": {
input: []v1beta1.WorkflowStep{},
app: &v1beta1.Application{
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{{
Name: "example-comp",
Type: "ref-objects",
}},
},
},
output: []v1beta1.WorkflowStep{{
Name: "deploy",
Type: "deploy",
Properties: &runtime.RawExtension{Raw: []byte(`{"policies":[]}`)},
}},
},
"pre-approve-workflow": {
input: []v1beta1.WorkflowStep{{
Name: "deploy-example-topology-policy-1",

View File

@ -322,4 +322,35 @@ var _ = Describe("Test multicluster standalone scenario", func() {
g.Expect(errors.IsNotFound(err)).Should(BeTrue())
}, time.Minute).Should(Succeed())
})
It("Test ref-objects with url", func() {
newApp := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "app"},
Spec: v1beta1.ApplicationSpec{
Components: []oamcomm.ApplicationComponent{{
Name: "example",
Type: "ref-objects",
Properties: &runtime.RawExtension{Raw: []byte(`{"urls":["https://gist.githubusercontent.com/Somefive/b189219a9222eaa70b8908cf4379402b/raw/920e83b1a2d56b584f9d8c7a97810a505a0bbaad/example-busybox-resources.yaml"]}`)},
}},
},
}
By("Create application")
Expect(k8sClient.Create(hubCtx, newApp)).Should(Succeed())
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(hubCtx, client.ObjectKeyFromObject(newApp), newApp)).Should(Succeed())
g.Expect(newApp.Status.Phase).Should(Equal(oamcomm.ApplicationRunning))
}, 15*time.Second).Should(Succeed())
key := types.NamespacedName{Namespace: namespace, Name: "busybox"}
Expect(k8sClient.Get(hubCtx, key, &v1.Deployment{})).Should(Succeed())
Expect(k8sClient.Get(hubCtx, key, &corev1.ConfigMap{})).Should(Succeed())
By("Delete application")
Expect(k8sClient.Delete(hubCtx, newApp)).Should(Succeed())
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(hubCtx, client.ObjectKeyFromObject(newApp), newApp)).Should(Satisfy(errors.IsNotFound))
}, 15*time.Second).Should(Succeed())
Expect(k8sClient.Get(hubCtx, key, &v1.Deployment{})).Should(Satisfy(errors.IsNotFound))
Expect(k8sClient.Get(hubCtx, key, &corev1.ConfigMap{})).Should(Satisfy(errors.IsNotFound))
})
})

View File

@ -64,7 +64,12 @@ template: {
...
}
output: parameter.objects[0]
output: {
if len(parameter.objects) > 0 {
parameter.objects[0]
}
...
}
outputs: {
for i, v in parameter.objects {