diff --git a/config/v3_2_experimental/schema/ignition.json b/config/v3_2_experimental/schema/ignition.json index 495b040f81..2e458c5034 100644 --- a/config/v3_2_experimental/schema/ignition.json +++ b/config/v3_2_experimental/schema/ignition.json @@ -421,7 +421,7 @@ "type": ["boolean", "null"] }, "contents": { - "type": ["string", "null"] + "$ref": "#/definitions/resource" }, "dropins": { "type": "array", @@ -441,7 +441,7 @@ "type": "string" }, "contents": { - "type": ["string", "null"] + "$ref": "#/definitions/resource" } }, "required": [ diff --git a/config/v3_2_experimental/types/schema.go b/config/v3_2_experimental/types/schema.go index fe2d3fdb45..0ab258ce22 100644 --- a/config/v3_2_experimental/types/schema.go +++ b/config/v3_2_experimental/types/schema.go @@ -27,8 +27,8 @@ type Disk struct { } type Dropin struct { - Contents *string `json:"contents,omitempty"` - Name string `json:"name"` + Contents Resource `json:"contents,omitempty"` + Name string `json:"name"` } type File struct { @@ -199,7 +199,7 @@ type Timeouts struct { } type Unit struct { - Contents *string `json:"contents,omitempty"` + Contents Resource `json:"contents,omitempty"` Dropins []Dropin `json:"dropins,omitempty"` Enabled *bool `json:"enabled,omitempty"` Mask *bool `json:"mask,omitempty"` diff --git a/config/v3_2_experimental/types/unit.go b/config/v3_2_experimental/types/unit.go index 8d1a5f00ff..3e47c796d8 100644 --- a/config/v3_2_experimental/types/unit.go +++ b/config/v3_2_experimental/types/unit.go @@ -15,14 +15,10 @@ package types import ( - "fmt" "path" - "strings" "github.com/coreos/ignition/v2/config/shared/errors" - "github.com/coreos/ignition/v2/config/shared/validations" - "github.com/coreos/go-systemd/v22/unit" cpath "github.com/coreos/vcontext/path" "github.com/coreos/vcontext/report" ) @@ -36,14 +32,12 @@ func (d Dropin) Key() string { } func (u Unit) Validate(c cpath.ContextPath) (r report.Report) { + var err error r.AddOnError(c.Append("name"), validateName(u.Name)) + r.Merge(u.Contents.Validate(c)) c = c.Append("contents") - opts, err := validateUnitContent(u.Contents) r.AddOnError(c, err) - isEnabled := u.Enabled != nil && *u.Enabled - r.AddOnWarn(c, validations.ValidateInstallSection(u.Name, isEnabled, (u.Contents == nil || *u.Contents == ""), opts)) - return } @@ -57,9 +51,6 @@ func validateName(name string) error { } func (d Dropin) Validate(c cpath.ContextPath) (r report.Report) { - _, err := validateUnitContent(d.Contents) - r.AddOnError(c.Append("contents"), err) - switch path.Ext(d.Name) { case ".conf": default: @@ -68,15 +59,3 @@ func (d Dropin) Validate(c cpath.ContextPath) (r report.Report) { return } - -func validateUnitContent(content *string) ([]*unit.UnitOption, error) { - if content == nil { - return []*unit.UnitOption{}, nil - } - c := strings.NewReader(*content) - opts, err := unit.Deserialize(c) - if err != nil { - return nil, fmt.Errorf("invalid unit content: %s", err) - } - return opts, nil -} diff --git a/doc/configuration-v3_2_experimental.md b/doc/configuration-v3_2_experimental.md index fef50f3789..9e126d2f43 100644 --- a/doc/configuration-v3_2_experimental.md +++ b/doc/configuration-v3_2_experimental.md @@ -121,9 +121,23 @@ The Ignition configuration is a JSON document conforming to the following specif * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. * **_contents_** (string): the contents of the unit. + * **_compression_** (string): the type of compression used on the contents (null or gzip). Compression cannot be used with S3. + * **_source_** (string): the URL of the file contents. Supported schemes are `http`, `https`, `tftp`, `s3`, and [`data`][rfc2397]. When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. + * **_httpHeaders_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file contents. + * **_hash_** (string): the hash of the contents, in the form `-` where type is either `sha512` or `sha256`. * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. * **name** (string): the name of the drop-in. This must be suffixed with ".conf". * **_contents_** (string): the contents of the drop-in. + * **_compression_** (string): the type of compression used on the contents (null or gzip). Compression cannot be used with S3. + * **_source_** (string): the URL of the file contents. Supported schemes are `http`, `https`, `tftp`, `s3`, and [`data`][rfc2397]. When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. + * **_httpHeaders_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file contents. + * **_hash_** (string): the hash of the contents, in the form `-` where type is either `sha512` or `sha256`. * **_passwd_** (object): describes the desired additions to the passwd database. * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. * **name** (string): the username for the account. diff --git a/internal/exec/stages/files/units.go b/internal/exec/stages/files/units.go index 477a3ef4b3..cbc1cfc8a1 100644 --- a/internal/exec/stages/files/units.go +++ b/internal/exec/stages/files/units.go @@ -21,6 +21,7 @@ import ( "github.com/coreos/ignition/v2/config/shared/errors" "github.com/coreos/ignition/v2/config/v3_2_experimental/types" + cUtil "github.com/coreos/ignition/v2/config/util" "github.com/coreos/ignition/v2/internal/distro" "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/systemd" @@ -183,7 +184,7 @@ func (s *stage) writeSystemdUnit(unit types.Unit, runtime bool) error { return s.Logger.LogOp(func() error { relabeledDropinDir := false for _, dropin := range unit.Dropins { - if dropin.Contents == nil || *dropin.Contents == "" { + if cUtil.NilOrEmpty(dropin.Contents.Source) { continue } f, err := u.FileFromSystemdUnitDropin(unit, dropin, runtime) @@ -211,7 +212,7 @@ func (s *stage) writeSystemdUnit(unit types.Unit, runtime bool) error { } } - if unit.Contents == nil || *unit.Contents == "" { + if cUtil.NilOrEmpty(unit.Contents.Source) { return nil } diff --git a/internal/exec/util/unit.go b/internal/exec/util/unit.go index 1b4b53d769..8c17259bbe 100644 --- a/internal/exec/util/unit.go +++ b/internal/exec/util/unit.go @@ -15,6 +15,7 @@ package util import ( + "encoding/hex" "fmt" "net/url" "os" @@ -22,8 +23,8 @@ import ( "github.com/coreos/ignition/v2/config/v3_2_experimental/types" "github.com/coreos/ignition/v2/internal/distro" - - "github.com/vincent-petithory/dataurl" + "github.com/coreos/ignition/v2/internal/resource" + "github.com/coreos/ignition/v2/internal/util" ) const ( @@ -31,46 +32,68 @@ const ( DefaultPresetPermissions os.FileMode = 0644 ) -func (ut Util) FileFromSystemdUnit(unit types.Unit, runtime bool) (FetchOp, error) { - if unit.Contents == nil { - empty := "" - unit.Contents = &empty +func (ut Util) getUnitFetch(path string, contents types.Resource) (FetchOp, error) { + u, err := url.Parse(*contents.Source) + if err != nil { + ut.Logger.Crit("Unable to parse systemd contents URL: %s", err) + return FetchOp{}, err } - u, err := url.Parse(dataurl.EncodeBytes([]byte(*unit.Contents))) + hasher, err := util.GetHasher(contents.Verification) if err != nil { + ut.Logger.Crit("Unable to get hasher: %s", err) return FetchOp{}, err } - var path string - if runtime { - path = SystemdRuntimeUnitsPath() - } else { - path = SystemdUnitsPath() + var expectedSum []byte + if hasher != nil { + // explicitly ignoring the error here because the config should already + // be validated by this point + _, expectedSumString, _ := util.HashParts(contents.Verification) + expectedSum, err = hex.DecodeString(expectedSumString) + if err != nil { + ut.Logger.Crit("Error parsing verification string %q: %v", expectedSumString, err) + return FetchOp{}, err + } } - if path, err = ut.JoinPath(path, unit.Name); err != nil { - return FetchOp{}, err + var compression string + if contents.Compression != nil { + compression = *contents.Compression } return FetchOp{ + Hash: hasher, Node: types.Node{ Path: path, }, Url: *u, + FetchOptions: resource.FetchOptions{ + Hash: hasher, + ExpectedSum: expectedSum, + Compression: compression, + }, }, nil } -func (ut Util) FileFromSystemdUnitDropin(unit types.Unit, dropin types.Dropin, runtime bool) (FetchOp, error) { - if dropin.Contents == nil { - empty := "" - dropin.Contents = &empty +func (ut Util) FileFromSystemdUnit(unit types.Unit, runtime bool) (FetchOp, error) { + var path string + var err error + if runtime { + path = SystemdRuntimeUnitsPath() + } else { + path = SystemdUnitsPath() } - u, err := url.Parse(dataurl.EncodeBytes([]byte(*dropin.Contents))) - if err != nil { + + if path, err = ut.JoinPath(path, unit.Name); err != nil { return FetchOp{}, err } + return ut.getUnitFetch(path, unit.Contents) +} + +func (ut Util) FileFromSystemdUnitDropin(unit types.Unit, dropin types.Dropin, runtime bool) (FetchOp, error) { var path string + var err error if runtime { path = SystemdRuntimeDropinsPath(string(unit.Name)) } else { @@ -81,12 +104,7 @@ func (ut Util) FileFromSystemdUnitDropin(unit types.Unit, dropin types.Dropin, r return FetchOp{}, err } - return FetchOp{ - Node: types.Node{ - Path: path, - }, - Url: *u, - }, nil + return ut.getUnitFetch(path, unit.Contents) } // MaskUnit writes a symlink to /dev/null to mask the specified unit and returns the path of that unit