diff --git a/internal/project/install.go b/internal/project/install.go index f91231b..eee6443 100644 --- a/internal/project/install.go +++ b/internal/project/install.go @@ -276,8 +276,13 @@ func packageHasHealthyConditions(pkg xpkgv1.PackageRevision) bool { // ApplyResources installs arbitrary resources to the target control plane. func ApplyResources(ctx context.Context, cl client.Client, resources []runtime.RawExtension) error { for _, raw := range resources { + // Skip empty documents. A YAML manifest can contain documents that hold + // no resource — e.g. a leading comment block before the first "---" + // separator, or "---\n---" between two resources — which unmarshal to an + // empty RawExtension. kubectl skips these; do the same rather than + // rejecting an otherwise valid manifest. if len(raw.Raw) == 0 { - return errors.New("encountered an invalid or empty raw resource") + continue } obj := &unstructured.Unstructured{} diff --git a/internal/project/install_test.go b/internal/project/install_test.go index 8dc6d7c..b4bef50 100644 --- a/internal/project/install_test.go +++ b/internal/project/install_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -103,15 +104,38 @@ func TestApplyResources(t *testing.T) { } }) - t.Run("EmptyRawErrors", func(t *testing.T) { - t.Parallel() + // Empty documents (e.g. a leading comment block before the first "---") + // unmarshal to an empty RawExtension. They must be skipped rather than + // rejected, and resources alongside them still applied. + skipCases := map[string]struct { + reason string + resources []runtime.RawExtension + wantErr error + wantName string + }{ + "SkipsEmptyDocumentAndAppliesRest": { + reason: "An empty document alongside a real resource is skipped, and the real resource is still applied.", + resources: []runtime.RawExtension{{}, {Raw: configMapJSON("real")}}, + wantErr: nil, + wantName: "real", + }, + } + for name, tc := range skipCases { + t.Run(name, func(t *testing.T) { + t.Parallel() - cl := newFakeClient(t) - err := ApplyResources(t.Context(), cl, []runtime.RawExtension{{}}) - if err == nil { - t.Fatal("expected error for empty raw, got nil") - } - }) + cl := newFakeClient(t) + err := ApplyResources(t.Context(), cl, tc.resources) + if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("%s\nApplyResources(...): -want error, +got error:\n%s", tc.reason, diff) + } + + var got corev1.ConfigMap + if err := cl.Get(t.Context(), types.NamespacedName{Name: tc.wantName, Namespace: "default"}, &got); err != nil { + t.Errorf("%s\nconfigmap %q not found: %v", tc.reason, tc.wantName, err) + } + }) + } } func TestLookupLockPackage(t *testing.T) {