diff --git a/pkg/kube/client.go b/pkg/kube/client.go index ac6bd5ebd..34d32c5d7 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -39,6 +39,7 @@ import ( batch "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" extv1beta1 "k8s.io/api/extensions/v1beta1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" @@ -81,16 +82,24 @@ type Client struct { Log func(string, ...interface{}) } +var addToScheme sync.Once + // New creates a new Client. func New(getter genericclioptions.RESTClientGetter) *Client { if getter == nil { getter = genericclioptions.NewConfigFlags(true) } - err := apiextv1beta1.AddToScheme(scheme.Scheme) - if err != nil { - panic(err) - } + // Add CRDs to the scheme. They are missing by default. + addToScheme.Do(func() { + if err := apiextv1.AddToScheme(scheme.Scheme); err != nil { + // This should never happen. + panic(err) + } + if err := apiextv1beta1.AddToScheme(scheme.Scheme); err != nil { + panic(err) + } + }) return &Client{ Factory: cmdutil.NewFactory(getter), @@ -385,7 +394,7 @@ func (c *Client) Get(namespace string, reader io.Reader) (string, error) { return nil } - //Get the relation pods + // Get the relation pods objs, err = c.getSelectRelationPod(info, objs) if err != nil { c.Log("Warning: get the relation pod is failed, err:%s", err.Error()) @@ -733,32 +742,20 @@ func (c *Client) pollCRDEstablished(t time.Duration) ResourceActorFunc { } func (c *Client) pollCRDUntilEstablished(timeout time.Duration, info *resource.Info) error { - return wait.PollImmediate(time.Second, timeout, func() (bool, error) { - err := info.Get() - if err != nil { - return false, fmt.Errorf("unable to get CRD: %v", err) - } - - crd := &apiextv1beta1.CustomResourceDefinition{} - err = scheme.Scheme.Convert(info.Object, crd, nil) - if err != nil { - return false, fmt.Errorf("unable to convert to CRD type: %v", err) - } + obj, err := asVersioned(info) + if err != nil { + return fmt.Errorf("unable to get versioned CRD: %v", err) + } - for _, cond := range crd.Status.Conditions { - switch cond.Type { - case apiextv1beta1.Established: - if cond.Status == apiextv1beta1.ConditionTrue { - return true, nil - } - case apiextv1beta1.NamesAccepted: - if cond.Status == apiextv1beta1.ConditionFalse { - return false, fmt.Errorf("naming conflict detected for CRD %s", crd.GetName()) - } - } + return wait.PollImmediate(time.Second, timeout, func() (bool, error) { + switch value := obj.(type) { + case *apiextv1beta1.CustomResourceDefinition: + return c.crdBetaReady(*value), nil + case *apiextv1.CustomResourceDefinition: + return c.crdReady(*value), nil + default: + return false, fmt.Errorf("invalid CRD kind: %T", value) } - - return false, nil }) } @@ -839,10 +836,11 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P _, isUnstructured := versionedObject.(runtime.Unstructured) // On newer K8s versions, CRDs aren't unstructured but has this dedicated type - _, isCRD := versionedObject.(*apiextv1beta1.CustomResourceDefinition) + _, isV1CRD := versionedObject.(*apiextv1.CustomResourceDefinition) + _, isV1Beta1CRD := versionedObject.(*apiextv1beta1.CustomResourceDefinition) switch { - case runtime.IsNotRegisteredError(err), isUnstructured, isCRD: + case runtime.IsNotRegisteredError(err), isUnstructured, isV1CRD, isV1Beta1CRD: // fall back to generic JSON merge patch patch, err := jsonpatch.CreateMergePatch(oldData, newData) if err != nil { @@ -1171,6 +1169,49 @@ func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]run return objs, nil } +// Because the v1 extensions API is not available on all supported k8s versions +// yet and because Go doesn't support generics, we need to have a duplicate +// function to support the v1beta1 types +func (c *Client) crdBetaReady(crd apiextv1beta1.CustomResourceDefinition) bool { + for _, cond := range crd.Status.Conditions { + switch cond.Type { + case apiextv1beta1.Established: + if cond.Status == apiextv1beta1.ConditionTrue { + return true + } + case apiextv1beta1.NamesAccepted: + if cond.Status == apiextv1beta1.ConditionFalse { + // This indicates a naming conflict, but it's probably not the + // job of this function to fail because of that. Instead, + // we treat it as a success, since the process should be able to + // continue. + return true + } + } + } + return false +} + +func (c *Client) crdReady(crd apiextv1.CustomResourceDefinition) bool { + for _, cond := range crd.Status.Conditions { + switch cond.Type { + case apiextv1.Established: + if cond.Status == apiextv1.ConditionTrue { + return true + } + case apiextv1.NamesAccepted: + if cond.Status == apiextv1.ConditionFalse { + // This indicates a naming conflict, but it's probably not the + // job of this function to fail because of that. Instead, + // we treat it as a success, since the process should be able to + // continue. + return true + } + } + } + return false +} + func asVersionedOrUnstructured(info *resource.Info) runtime.Object { obj, _ := asVersioned(info) return obj diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 5add37283..c5c12a13f 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -27,6 +27,7 @@ import ( "time" v1 "k8s.io/api/core/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -43,6 +44,10 @@ func init() { if err != nil { panic(err) } + err = apiextv1.AddToScheme(scheme.Scheme) + if err != nil { + panic(err) + } // Tiller use the scheme from go-client, but the cmdtesting // package used here is hardcoded to use the scheme from @@ -52,6 +57,10 @@ func init() { if err != nil { panic(err) } + err = apiextv1.AddToScheme(kubectlscheme.Scheme) + if err != nil { + panic(err) + } } var ( @@ -533,32 +542,64 @@ func TestResourceSortOrder(t *testing.T) { func TestWaitUntilCRDEstablished(t *testing.T) { testCases := map[string]struct { - conditions []apiextv1beta1.CustomResourceDefinitionCondition + conditions []apiextv1.CustomResourceDefinitionCondition + crdManifest string returnConditionsAfter int success bool }{ - "crd reaches established state after 2 requests": { - conditions: []apiextv1beta1.CustomResourceDefinitionCondition{ + "v1beta1 crd reaches established state after 2 requests": { + conditions: []apiextv1.CustomResourceDefinitionCondition{ { - Type: apiextv1beta1.Established, - Status: apiextv1beta1.ConditionTrue, + Type: apiextv1.Established, + Status: apiextv1.ConditionTrue, }, }, + crdManifest: crdV1Beta1Manifest, returnConditionsAfter: 2, success: true, }, - "crd does not reach established state before timeout": { - conditions: []apiextv1beta1.CustomResourceDefinitionCondition{}, + "v1beta1 crd does not reach established state before timeout": { + conditions: []apiextv1.CustomResourceDefinitionCondition{}, + crdManifest: crdV1Beta1Manifest, returnConditionsAfter: 100, success: false, }, - "crd name is not accepted": { - conditions: []apiextv1beta1.CustomResourceDefinitionCondition{ + "v1beta1 crd name is not accepted": { + conditions: []apiextv1.CustomResourceDefinitionCondition{ { - Type: apiextv1beta1.NamesAccepted, - Status: apiextv1beta1.ConditionFalse, + Type: apiextv1.NamesAccepted, + Status: apiextv1.ConditionFalse, }, }, + crdManifest: crdV1Beta1Manifest, + returnConditionsAfter: 1, + success: false, + }, + "v1 crd reaches established state after 2 requests": { + conditions: []apiextv1.CustomResourceDefinitionCondition{ + { + Type: apiextv1.Established, + Status: apiextv1.ConditionTrue, + }, + }, + crdManifest: crdV1Manifest, + returnConditionsAfter: 2, + success: true, + }, + "v1 crd does not reach established state before timeout": { + conditions: []apiextv1.CustomResourceDefinitionCondition{}, + crdManifest: crdV1Manifest, + returnConditionsAfter: 100, + success: false, + }, + "v1 crd name is not accepted": { + conditions: []apiextv1.CustomResourceDefinitionCondition{ + { + Type: apiextv1.NamesAccepted, + Status: apiextv1.ConditionFalse, + }, + }, + crdManifest: crdV1Manifest, returnConditionsAfter: 1, success: false, }, @@ -569,8 +610,8 @@ func TestWaitUntilCRDEstablished(t *testing.T) { c := newTestClient() defer c.Cleanup() - crdWithoutConditions := newCrdWithStatus("name", apiextv1beta1.CustomResourceDefinitionStatus{}) - crdWithConditions := newCrdWithStatus("name", apiextv1beta1.CustomResourceDefinitionStatus{ + crdWithoutConditions := newCrdWithStatus("name", apiextv1.CustomResourceDefinitionStatus{}) + crdWithConditions := newCrdWithStatus("name", apiextv1.CustomResourceDefinitionStatus{ Conditions: tc.conditions, }) @@ -579,7 +620,7 @@ func TestWaitUntilCRDEstablished(t *testing.T) { GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: unstructuredSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - var crd apiextv1beta1.CustomResourceDefinition + var crd apiextv1.CustomResourceDefinition if requestCount < tc.returnConditionsAfter { crd = crdWithoutConditions } else { @@ -590,7 +631,7 @@ func TestWaitUntilCRDEstablished(t *testing.T) { }), } - err := c.WaitUntilCRDEstablished(strings.NewReader(crdManifest), 5*time.Second) + err := c.WaitUntilCRDEstablished(strings.NewReader(crdV1Beta1Manifest), 5*time.Second) if err != nil && tc.success { t.Errorf("%s: expected no error, but got %v", name, err) } @@ -601,13 +642,13 @@ func TestWaitUntilCRDEstablished(t *testing.T) { } } -func newCrdWithStatus(name string, status apiextv1beta1.CustomResourceDefinitionStatus) apiextv1beta1.CustomResourceDefinition { - crd := apiextv1beta1.CustomResourceDefinition{ +func newCrdWithStatus(name string, status apiextv1.CustomResourceDefinitionStatus) apiextv1.CustomResourceDefinition { + crd := apiextv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: metav1.NamespaceDefault, }, - Spec: apiextv1beta1.CustomResourceDefinitionSpec{}, + Spec: apiextv1.CustomResourceDefinitionSpec{}, Status: status, } return crd @@ -881,7 +922,7 @@ spec: - containerPort: 80 ` -const crdManifest = ` +const crdV1Beta1Manifest = ` apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: @@ -918,3 +959,41 @@ status: conditions: [] storedVersions: [] ` + +const crdV1Manifest = ` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + creationTimestamp: null + labels: + controller-tools.k8s.io: "1.0" + name: applications.app.k8s.io +spec: + group: app.k8s.io + names: + kind: Application + plural: applications + scope: Namespaced + validation: + openAPIV3Schema: + properties: + apiVersion: + description: 'Description' + type: string + kind: + description: 'Kind' + type: string + metadata: + type: object + spec: + type: object + status: + type: object + version: v1beta1 +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +`