Skip to content

Commit a06a91f

Browse files
committed
Add support for syncing objects from kubernetes components to cluster
Add support for syncing Kubernetes/OpenShift DevWorkspace components to the cluster. If the object is one "recognized" by DWO, it is synced using the same procedure as regular DevWorkspace Objects; otherwise the workspace gets a warning and only the metadata is kept in sync. Signed-off-by: Angel Misevski <[email protected]>
1 parent 3d88cf5 commit a06a91f

File tree

3 files changed

+234
-0
lines changed

3 files changed

+234
-0
lines changed

pkg/library/kubernetes/errors.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2019-2022 Red Hat, Inc.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package kubernetes
15+
16+
import "github.com/devfile/devworkspace-operator/pkg/provision/sync"
17+
18+
type RetryError struct {
19+
Err error
20+
}
21+
22+
func (e *RetryError) Error() string {
23+
return e.Err.Error()
24+
}
25+
26+
type FailError struct {
27+
Err error
28+
}
29+
30+
func (e *FailError) Error() string {
31+
return e.Err.Error()
32+
}
33+
34+
func wrapSyncError(err error) error {
35+
switch syncErr := err.(type) {
36+
case *sync.NotInSyncError:
37+
return &RetryError{syncErr}
38+
case *sync.UnrecoverableSyncError:
39+
return &FailError{syncErr}
40+
case *sync.WarningError:
41+
return &WarningError{syncErr}
42+
default:
43+
return err
44+
}
45+
}
46+
47+
type WarningError struct {
48+
Err error
49+
}
50+
51+
func (e *WarningError) Error() string {
52+
return e.Err.Error()
53+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (c) 2019-2022 Red Hat, Inc.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package kubernetes
15+
16+
import (
17+
"fmt"
18+
"reflect"
19+
20+
"github.com/devfile/devworkspace-operator/pkg/common"
21+
"github.com/devfile/devworkspace-operator/pkg/constants"
22+
"github.com/devfile/devworkspace-operator/pkg/provision/sync"
23+
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
25+
crclient "sigs.k8s.io/controller-runtime/pkg/client"
26+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
27+
)
28+
29+
// HandleKubernetesComponents processes the Kubernetes and OpenShift components of a DevWorkspace,
30+
// creating/updating objects on the cluster. This function does not verify if the workspace owner
31+
// has the correct permissions to create/update/delete these objects and instead assumes the
32+
// workspace owner has all applicable RBAC permissions.
33+
// Only Kubernetes/OpenShift components that are inlined are supported; components that define
34+
// a URI will cause a FailError to be returned
35+
func HandleKubernetesComponents(workspace *common.DevWorkspaceWithConfig, api sync.ClusterAPI) error {
36+
kubeComponents, err := filterForKubeLikeComponents(workspace.Spec.Template.Components)
37+
if err != nil {
38+
return err
39+
}
40+
if len(kubeComponents) == 0 {
41+
return nil
42+
}
43+
for _, component := range kubeComponents {
44+
// Ignore error as we filtered list above
45+
k8sLikeComponent, _ := getK8sLikeComponent(component)
46+
obj, err := deserializeToObject([]byte(k8sLikeComponent.Inlined), api)
47+
if err != nil {
48+
return &FailError{fmt.Errorf("could not process component %s: %w", component.Name, err)}
49+
}
50+
if err := addMetadata(obj, workspace, api); err != nil {
51+
return &RetryError{fmt.Errorf("failed to add ownerref for component %s: %w", component.Name, err)}
52+
}
53+
if err := checkForExistingObject(obj, api); err != nil {
54+
return &FailError{fmt.Errorf("could not process component %s: %w", component.Name, err)}
55+
}
56+
var syncErr error
57+
if sync.IsRecognizedObject(obj) {
58+
_, syncErr = sync.SyncObjectWithCluster(obj, api)
59+
} else {
60+
_, syncErr = sync.SyncUnrecognizedObjectWithCluster(obj, api)
61+
}
62+
if syncErr != nil {
63+
return wrapSyncError(syncErr)
64+
}
65+
}
66+
return nil
67+
}
68+
69+
func checkForExistingObject(obj client.Object, api sync.ClusterAPI) error {
70+
objType := reflect.TypeOf(obj).Elem()
71+
clusterObj := reflect.New(objType).Interface().(crclient.Object)
72+
err := api.Client.Get(api.Ctx, client.ObjectKey{Name: obj.GetName(), Namespace: obj.GetNamespace()}, clusterObj)
73+
switch {
74+
case err == nil:
75+
break
76+
case k8sErrors.IsNotFound(err):
77+
// Object does not exist yet; safe to create
78+
return nil
79+
default:
80+
return err
81+
}
82+
existingWorkspaceID := clusterObj.GetLabels()[constants.DevWorkspaceIDLabel]
83+
expectedWorkspaceID := obj.GetLabels()[constants.DevWorkspaceIDLabel]
84+
if existingWorkspaceID != expectedWorkspaceID {
85+
return fmt.Errorf("object %s exists and is not owned by this workspace", obj.GetName())
86+
}
87+
if err := checkOwnerrefs(clusterObj.GetOwnerReferences(), obj.GetOwnerReferences()); err != nil {
88+
return fmt.Errorf("object %s exists and is not owned by this workspace", obj.GetName())
89+
}
90+
return nil
91+
}
92+
93+
func addMetadata(obj client.Object, workspace *common.DevWorkspaceWithConfig, api sync.ClusterAPI) error {
94+
obj.SetNamespace(workspace.Namespace)
95+
if err := controllerutil.SetOwnerReference(workspace.DevWorkspace, obj, api.Scheme); err != nil {
96+
return err
97+
}
98+
newLabels := map[string]string{}
99+
for k, v := range obj.GetLabels() {
100+
newLabels[k] = v
101+
}
102+
newLabels[constants.DevWorkspaceIDLabel] = workspace.Status.DevWorkspaceId
103+
newLabels[constants.DevWorkspaceCreatorLabel] = workspace.Labels[constants.DevWorkspaceCreatorLabel]
104+
if obj.GetObjectKind().GroupVersionKind().Kind == "Secret" {
105+
newLabels[constants.DevWorkspaceWatchSecretLabel] = "true"
106+
}
107+
if obj.GetObjectKind().GroupVersionKind().Kind == "ConfigMap" {
108+
newLabels[constants.DevWorkspaceWatchConfigMapLabel] = "true"
109+
}
110+
obj.SetLabels(newLabels)
111+
return nil
112+
}

pkg/library/kubernetes/util.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) 2019-2022 Red Hat, Inc.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package kubernetes
15+
16+
import (
17+
"fmt"
18+
19+
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
22+
23+
func filterForKubeLikeComponents(components []dw.Component) ([]dw.Component, error) {
24+
var k8sLikeComponents []dw.Component
25+
for _, component := range components {
26+
k8sLikeComponent, err := getK8sLikeComponent(component)
27+
if err != nil {
28+
continue
29+
}
30+
if k8sLikeComponent.Uri != "" {
31+
return nil, &FailError{fmt.Errorf("kubernetes/openshift components that define a URI are unsupported (component %s)", component.Name)}
32+
}
33+
if k8sLikeComponent.Inlined == "" {
34+
continue
35+
}
36+
k8sLikeComponents = append(k8sLikeComponents, component)
37+
}
38+
return k8sLikeComponents, nil
39+
}
40+
41+
// getK8sLikeComponent returns the K8sLikeComponent from a DevWorkspace component,
42+
// allowing Kubernetes and OpenShift components to be processed in the same way.
43+
// Returns error if component is not a kube-like component.
44+
func getK8sLikeComponent(component dw.Component) (*dw.K8sLikeComponent, error) {
45+
switch {
46+
case component.Kubernetes != nil:
47+
return &component.Kubernetes.K8sLikeComponent, nil
48+
case component.Openshift != nil:
49+
return &component.Openshift.K8sLikeComponent, nil
50+
default:
51+
return nil, fmt.Errorf("not a kube-like component")
52+
}
53+
}
54+
55+
func checkOwnerrefs(ownerrefs, subset []metav1.OwnerReference) error {
56+
for _, checkOwnerref := range subset {
57+
found := false
58+
for _, ownerref := range ownerrefs {
59+
if ownerref.Name == checkOwnerref.Name && ownerref.UID == checkOwnerref.UID {
60+
found = true
61+
break
62+
}
63+
}
64+
if !found {
65+
return fmt.Errorf("ownerref not found")
66+
}
67+
}
68+
return nil
69+
}

0 commit comments

Comments
 (0)