-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
bundle.go
329 lines (282 loc) · 11.4 KB
/
bundle.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
// Copyright 2020 The Operator-SDK Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bundle
import (
"errors"
"fmt"
"os"
"path/filepath"
"sigs.k8s.io/yaml"
"github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3"
"github.com/operator-framework/operator-manifest-tools/pkg/image"
"github.com/operator-framework/operator-manifest-tools/pkg/imageresolver"
"github.com/operator-framework/operator-manifest-tools/pkg/pullspec"
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
metricsannotations "github.com/operator-framework/operator-sdk/internal/annotations/metrics"
genutil "github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/generate/internal"
gencsv "github.com/operator-framework/operator-sdk/internal/generate/clusterserviceversion"
"github.com/operator-framework/operator-sdk/internal/generate/clusterserviceversion/bases"
"github.com/operator-framework/operator-sdk/internal/generate/collector"
"github.com/operator-framework/operator-sdk/internal/registry"
"github.com/operator-framework/operator-sdk/internal/scorecard"
"github.com/operator-framework/operator-sdk/internal/util/bundleutil"
)
const (
longHelp = `
Running 'generate bundle' is the first step to publishing your operator to a catalog and deploying it with OLM.
This command both generates and packages files into an on-disk representation of an operator called a bundle.
A bundle consists of a ClusterServiceVersion (CSV), CustomResourceDefinitions (CRDs),
manifests not part of the CSV but required by the operator, some metadata (annotations.yaml),
and a bundle.Dockerfile to build a bundle image.
A CSV manifest is generated by collecting data from the set of manifests passed to this command (see below),
such as CRDs, RBAC, etc., and applying that data to a "base" CSV manifest. This base CSV can contain metadata,
added by hand or by the 'generate kustomize manifests' command, and can be passed in like any other manifest
(see below) or by file at the exact path '<kustomize-dir>/bases/<package-name>.clusterserviceversion.yaml'.
Be aware that 'generate bundle' idempotently regenerates a bundle, so all non-metadata values in a base
will be overwritten. If no base was passed in, input manifest data will be applied to an empty CSV.
There are two ways to pass the to-be-bundled set of manifests to this command: stdin via a Unix pipe,
or in a directory using '--input-dir'. See command help for more information on these modes.
Passing a directory is useful for running 'generate bundle' outside of a project or within a project
that does not use kustomize and/or contains cluster-ready manifests on disk.
Set '--version' to supply a semantic version for your bundle if you are creating one
for the first time or upgrading an existing one.
If '--output-dir' is set and you wish to build bundle images from that directory,
either manually update your bundle.Dockerfile or set '--overwrite'.
More information on bundles:
https://github.com/operator-framework/operator-registry/#manifest-format
`
examples = `
# If running within a project or in a project that uses kustomize to generate manifests,
# make sure a kustomize directory exists that looks like the following 'config/manifests' directory:
$ tree config/manifests
config/manifests
├── bases
│ └── memcached-operator.clusterserviceversion.yaml
└── kustomization.yaml
# Generate a 0.0.1 bundle by passing manifests to stdin:
$ kustomize build config/manifests | operator-sdk generate bundle --version 0.0.1
Generating bundle version 0.0.1
...
# If running outside of a project or in a project that does not use kustomize to generate manifests,
# make sure cluster-ready manifests are available on disk:
$ tree deploy/
deploy/
├── crds
│ └── cache.my.domain_memcacheds.yaml
├── deployment.yaml
├── role.yaml
├── role_binding.yaml
├── service_account.yaml
└── webhooks.yaml
# Generate a 0.0.1 bundle by passing manifests by dir:
$ operator-sdk generate bundle --input-dir deploy --version 0.0.1
Generating bundle version 0.0.1
...
# After running in either of the above modes, you should see this directory structure:
$ tree bundle/
bundle/
├── manifests
│ ├── cache.my.domain_memcacheds.yaml
│ └── memcached-operator.clusterserviceversion.yaml
└── metadata
└── annotations.yaml
`
)
// defaultRootDir is the default root directory in which to generate bundle files.
const defaultRootDir = "bundle"
// setDefaults sets defaults useful to all modes of this subcommand.
func (c *bundleCmd) setDefaults() (err error) {
if c.packageName, c.layout, err = genutil.GetPackageNameAndLayout(c.packageName); err != nil {
return err
}
return nil
}
// validateManifests validates c for bundle manifests generation.
func (c bundleCmd) validateManifests() (err error) {
if c.version != "" {
if err := genutil.ValidateVersion(c.version); err != nil {
return err
}
}
// The three possible usage modes (stdin, inputDir, and legacy dirs) are mutually exclusive
// and one must be chosen.
isPipeReader := genutil.IsPipeReader()
isInputDir := c.inputDir != ""
isLegacyDirs := c.deployDir != "" || c.crdsDir != ""
switch {
case !(isPipeReader || isInputDir || isLegacyDirs):
return errors.New("one of stdin, --input-dir, or --deploy-dir (and optionally --crds-dir) must be set")
case isPipeReader && (isInputDir || isLegacyDirs):
return errors.New("none of --input-dir, --deploy-dir, or --crds-dir may be set if reading from stdin")
case isInputDir && isLegacyDirs:
return errors.New("only one of --input-dir or --deploy-dir (and optionally --crds-dir) may be set if not reading from stdin")
}
if c.stdout {
if c.outputDir != "" {
return errors.New("--output-dir cannot be set if writing to stdout")
}
}
return nil
}
// TODO: Move this to bundleutil package
// runManifests generates bundle manifests.
func (c bundleCmd) runManifests() (err error) {
c.println("Generating bundle manifests")
if !c.stdout && c.outputDir == "" {
c.outputDir = defaultRootDir
}
col := &collector.Manifests{}
switch {
case genutil.IsPipeReader():
err = col.UpdateFromReader(os.Stdin)
case c.deployDir != "" && c.crdsDir != "":
err = col.UpdateFromDirs(c.deployDir, c.crdsDir)
case c.deployDir != "": // If only deployDir is set, use as input dir.
c.inputDir = c.deployDir
fallthrough
case c.inputDir != "":
err = col.UpdateFromDir(c.inputDir)
}
if err != nil {
return err
}
// If no CSV was initially read, a kustomize base can be used at the default base path.
// Only read from kustomizeDir if a base exists so users can still generate a barebones CSV.
baseCSVPath := filepath.Join(c.kustomizeDir, "bases", c.packageName+".clusterserviceversion.yaml")
if noCSVStdin := len(col.ClusterServiceVersions) == 0; noCSVStdin && genutil.IsExist(baseCSVPath) {
base, err := bases.ClusterServiceVersion{BasePath: baseCSVPath}.GetBase()
if err != nil {
return fmt.Errorf("error reading CSV base: %v", err)
}
col.ClusterServiceVersions = append(col.ClusterServiceVersions, *base)
} else if noCSVStdin {
c.println("Building a ClusterServiceVersion without an existing base")
}
relatedImages, err := genutil.FindRelatedImages(col)
if err != nil {
return err
}
var opts []gencsv.Option
stdout := genutil.NewMultiManifestWriter(os.Stdout)
if c.stdout {
opts = append(opts, gencsv.WithWriter(stdout))
} else {
opts = append(opts, gencsv.WithBundleWriter(c.outputDir))
}
csvGen := gencsv.Generator{
OperatorName: c.packageName,
Version: c.version,
Collector: col,
Annotations: metricsannotations.MakeBundleObjectAnnotations(c.layout),
ExtraServiceAccounts: c.extraServiceAccounts,
RelatedImages: relatedImages,
}
if err := csvGen.Generate(opts...); err != nil {
return fmt.Errorf("error generating ClusterServiceVersion: %v", err)
}
objs := genutil.GetManifestObjects(col, c.extraServiceAccounts)
if c.stdout {
if err := genutil.WriteObjects(stdout, objs...); err != nil {
return err
}
} else {
dir := filepath.Join(c.outputDir, bundle.ManifestsDir)
if err := genutil.WriteObjectsToFiles(dir, objs...); err != nil {
return err
}
}
// Pin images to digests if enabled
if c.useImageDigests {
c.println("pinning image versions to digests instead of tags")
if err := c.pinImages(filepath.Join(c.outputDir, "manifests")); err != nil {
return err
}
}
// Write the scorecard config if it was passed.
if err := writeScorecardConfig(c.outputDir, col.ScorecardConfig); err != nil {
return fmt.Errorf("error writing bundle scorecard config: %v", err)
}
c.println("Bundle manifests generated successfully in", c.outputDir)
return nil
}
// writeScorecardConfig writes cfg to dir at the hard-coded config path 'config.yaml'.
func writeScorecardConfig(dir string, cfg v1alpha3.Configuration) error {
// Skip writing if config is empty.
if cfg.Metadata.Name == "" {
return nil
}
b, err := yaml.Marshal(cfg)
if err != nil {
return err
}
cfgDir := filepath.Join(dir, filepath.FromSlash(scorecard.DefaultConfigDir))
if err := os.MkdirAll(cfgDir, 0755); err != nil {
return err
}
scorecardConfigPath := filepath.Join(cfgDir, scorecard.ConfigFileName)
return os.WriteFile(scorecardConfigPath, b, 0666)
}
// runMetadata generates a bundle.Dockerfile and bundle metadata.
func (c bundleCmd) runMetadata() error {
c.println("Generating bundle metadata")
if c.outputDir == "" {
c.outputDir = defaultRootDir
}
// If metadata already exists, only overwrite it if directed to.
bundleRoot := c.inputDir
if bundleRoot == "" {
bundleRoot = c.outputDir
}
// Find metadata from output directory only of it exists on disk.
if genutil.IsExist(bundleRoot) {
if _, _, err := registry.FindBundleMetadata(bundleRoot); err != nil {
merr := registry.MetadataNotFoundError("")
if !errors.As(err, &merr) {
return err
}
} else if !c.overwrite {
return nil
}
}
scorecardConfigPath := filepath.Join(bundleRoot, scorecard.DefaultConfigDir, scorecard.ConfigFileName)
bundleMetadata := bundleutil.BundleMetaData{
BundleDir: c.outputDir,
PackageName: c.packageName,
Channels: c.channels,
DefaultChannel: c.defaultChannel,
OtherLabels: metricsannotations.MakeBundleMetadataLabels(c.layout),
IsScoreConfigPresent: genutil.IsExist(scorecardConfigPath),
}
return bundleMetadata.GenerateMetadata()
}
// pinImages is used to replace all image tags in the given manifests with digests
func (c bundleCmd) pinImages(manifestPath string) error {
manifests, err := pullspec.FromDirectory(manifestPath, nil)
if err != nil {
return err
}
resolver, err := imageresolver.GetResolver(imageresolver.ResolverCrane, nil)
if err != nil {
return err
}
if err := image.Pin(resolver, manifests); err != nil {
return err
}
for _, manifest := range manifests {
if err := manifest.Dump(nil); err != nil {
return err
}
}
return nil
}