diff --git a/cmd/ignition2rpm/load.go b/cmd/ignition2rpm/load.go new file mode 100644 index 0000000..33590e8 --- /dev/null +++ b/cmd/ignition2rpm/load.go @@ -0,0 +1,201 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + + ign3types "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/golang/glog" + "gopkg.in/yaml.v2" + + resourceread "github.com/openshift/machine-config-operator/lib/resourceread" + mcfgv1 "github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1" + ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" + + butaneConfig "github.com/coreos/butane/config" + butaneOpts "github.com/coreos/butane/config/common" + "k8s.io/apimachinery/pkg/util/sets" +) + +type onceFromOrigin int + +const ( + onceFromUnknownConfig onceFromOrigin = iota + onceFromLocalConfig + onceFromRemoteConfig +) + +func processEnvelope(content interface{}, o *opts) (*ign3types.Config, error) { + // have to process the envelope if it's machineconfig, otherwise we don't + switch c := content.(type) { + case ign3types.Config: + return &c, nil + case mcfgv1.MachineConfig: + //shuck the ignition out of the machineconfig if need be + //TODO: fish the name out of the machineconfig for the metadata? + newIgnConfig, err := ctrlcommon.ParseAndConvertConfig(c.Spec.Config.Raw) + if err != nil { + return nil, fmt.Errorf("unable to convert machine config to ignition: %w", err) + } + + // If output RPM is not set and we have a machine config, use the machine + // config name + if o.outputRPM == "" { + o.outputRPM = c.GetName() + } + + return &newIgnConfig, nil + } + + return nil, fmt.Errorf("unknown input type") +} + +func loadConfig(onceFrom string) ([]byte, onceFromOrigin, error) { + var ( + contentSrc onceFromOrigin + err error + reader io.ReadCloser + ) + + defer func() { + if reader != nil { + reader.Close() + } + }() + + if strings.HasPrefix(onceFrom, "http://") || strings.HasPrefix(onceFrom, "https://") { + resp, err := http.Get(onceFrom) + if err != nil { + return []byte{}, onceFromRemoteConfig, err + } + + reader = resp.Body + contentSrc = onceFromRemoteConfig + } else { + // Otherwise read it from a local file + absoluteOnceFrom, err := filepath.Abs(filepath.Clean(onceFrom)) + if err != nil { + return []byte{}, onceFromLocalConfig, err + } + + local, err := os.Open(absoluteOnceFrom) + if err != nil { + return []byte{}, onceFromLocalConfig, err + } + + reader = local + contentSrc = onceFromLocalConfig + } + + content, err := ioutil.ReadAll(reader) + return content, contentSrc, err +} + +// blatantly stolen from mco daemon's once-from +func senseAndLoadOnceFrom(onceFrom string) (interface{}, onceFromOrigin, error) { + content, contentFrom, err := loadConfig(onceFrom) + if err != nil { + return nil, contentFrom, fmt.Errorf("could not load content: %w", err) + } + + // Try each supported parser + // Ignition + ignConfig, err := ctrlcommon.ParseAndConvertConfig(content) + if err == nil && ignConfig.Ignition.Version != "" { + glog.V(2).Info("onceFrom file is of type Ignition") + return ignConfig, contentFrom, nil + } + + // Machine Config + // Note: This will only read the first machine config in a given input file. + // The rest will be ignored. + mc, err := resourceread.ReadMachineConfigV1(content) + if err == nil && mc != nil { + glog.V(2).Info("onceFrom file is of type MachineConfig") + return *mc, contentFrom, nil + } + + // Butane + ign, err := parseButane(content) + if err == nil { + glog.V(2).Info("onceFrom file is of type Butane") + return *ign, contentFrom, nil + } + + return nil, onceFromUnknownConfig, fmt.Errorf("unable to decipher onceFrom config type: %w", err) +} + +func parseButane(butaneBytes []byte) (*ign3types.Config, error) { + // The following Butane variants and versions are unsupported: + // - FCOS 1.4.0 - parses into Ignition 3.3. + // - FCOS 1.5.0 - parses into Ignition 3.4 (experimental). + // - Openshift 4.10.0 - parses into Ingition 3.4 (experimental). + // + // The issue with supporting Ignition 3.3 and 3.4 is that the + // ctrlcommon.ParseAndConvertConfig() function assumes a max Ignition version + // of 3.2, so it throws an error. + // + // Translating to 3.3 / 3.4 is outside the scope, as is adding the support to + // ctrlcommon.ParseAndConvertConfig(). So for now, we'll just exclude those + // versions and variants. + // + // This pattern was inspired by: + // https://github.com/coreos/butane/blob/dcc128af5a36d81121e6af2ffa3305be74cb46dc/config/config.go + type butane struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + } + + b := butane{} + + if err := yaml.Unmarshal(butaneBytes, &b); err != nil { + return nil, err + } + + supportedButaneVersions := map[string]map[string]sets.Empty{ + "fcos": map[string]sets.Empty{ + "1.0.0": sets.Empty{}, + "1.1.0": sets.Empty{}, + "1.2.0": sets.Empty{}, + "1.3.0": sets.Empty{}, + }, + "openshift": map[string]sets.Empty{ + "4.8.0": sets.Empty{}, + "4.9.0": sets.Empty{}, + }, + "rhcos": map[string]sets.Empty{ + "0.1.0": sets.Empty{}, + }, + } + + versionsForVariant, ok := supportedButaneVersions[b.Variant] + if !ok { + return nil, fmt.Errorf("unsupported butane variant: %s; supported variants %s", b.Variant, sortedMapKeys(supportedButaneVersions)) + } + + if _, ok := versionsForVariant[b.Version]; !ok { + return nil, fmt.Errorf("unsupported butane version: %s for variant %s; supported versions %s", b.Version, b.Variant, sortedMapKeys(versionsForVariant)) + } + + ignBytes, _, err := butaneConfig.TranslateBytes(butaneBytes, butaneOpts.TranslateBytesOptions{ + // We always want Ignition configs, not MachineConfigs + // See: https://github.com/coreos/butane/blob/dcc128af5a36d81121e6af2ffa3305be74cb46dc/config/openshift/v4_8/translate.go#L139-L145 + Raw: true, + }) + + if err != nil { + return nil, err + } + + ignConfig, err := ctrlcommon.ParseAndConvertConfig(ignBytes) + return &ignConfig, err +} + +func sortedMapKeys(theMap interface{}) string { + return "(" + strings.Join(sets.StringKeySet(theMap).List(), ", ") + ")" +} diff --git a/cmd/ignition2rpm/main.go b/cmd/ignition2rpm/main.go index 7fce96a..43978f4 100644 --- a/cmd/ignition2rpm/main.go +++ b/cmd/ignition2rpm/main.go @@ -3,43 +3,46 @@ package main import ( "flag" "fmt" - "io/ioutil" - "net/http" "os" "path/filepath" - "strconv" "strings" - "time" - "github.com/vincent-petithory/dataurl" - - ign3types "github.com/coreos/ignition/v2/config/v3_2/types" "github.com/golang/glog" - "github.com/google/rpmpack" - - resourceread "github.com/openshift/machine-config-operator/lib/resourceread" - mcfgv1 "github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1" - ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" ) var Version string -var excludePrefix string + +type opts struct { + excludePrefix string + config string + outputRPM string + packageCanOverride bool +} + +func (o opts) isExcluded(path string) bool { + return o.excludePrefix != "" && strings.HasPrefix(path, o.excludePrefix) +} + +func (o opts) packageName() string { + //just the filename so we can use it as the package name + fileName := strings.TrimSuffix(o.config, filepath.Ext(o.config)) + + return filepath.Base(fileName) +} func main() { + o := opts{} - var config string - var outputRPM string var version bool - var packageCanOverride bool flag.Set("logtostderr", "true") flag.Set("stderrthreshold", "WARNING") flag.Set("v", "2") - flag.StringVar(&excludePrefix, "exclude-prefix", "", "Exclude files with this prefix") - flag.StringVar(&config, "config", "", "Config file ign/machineconfig to read") - flag.StringVar(&outputRPM, "output", "", "Specify name of RPM file to write") - flag.BoolVar(&packageCanOverride, "can-override", false, "Include fake 'provides' for rpm-ostree (https://github.com/coreos/rpm-ostree/pull/3125)") + flag.StringVar(&o.excludePrefix, "exclude-prefix", "", "Exclude files with this prefix") + flag.StringVar(&o.config, "config", "", "Config file ign/machineconfig to read") + flag.StringVar(&o.outputRPM, "output", "", "Specify name of RPM file to write (optional for MachineConfigs)") + flag.BoolVar(&o.packageCanOverride, "can-override", false, "Include fake 'provides' for rpm-ostree (https://github.com/coreos/rpm-ostree/pull/3125)") flag.BoolVar(&version, "version", false, "show the version ("+Version+")") flag.Parse() @@ -48,370 +51,44 @@ func main() { os.Exit(0) } - //just the filename so we can use it as the package name - fileName := strings.TrimSuffix(config, filepath.Ext(config)) - fileName = filepath.Base(fileName) - var err error // use the library functions to figure out what this is - configi, contentFrom, err := senseAndLoadOnceFrom(config) + configi, contentFrom, err := senseAndLoadOnceFrom(o.config) if err != nil { - glog.Fatalf("Unable to decipher onceFrom config type: %s", err) + glog.Fatal(err) } // I might use this later, but not yet _ = contentFrom - //just so we can set the build host - hostname, _ := os.Hostname() - - //specify some boilerplate metadata - packedRPM, err := rpmpack.NewRPM(rpmpack.RPMMetaData{ - Name: fileName, - Version: "1", - Release: "1", - Summary: "A package packed from " + config, - Description: "This is a machine-packed RPM that has been packed by 'ignition2rpm'", - BuildTime: time.Now(), - Packager: "MCO ignition2rpm", - Vendor: "RedHat OpenShift", - //Licence: "", - BuildHost: hostname, - }, - ) - - if err != nil { - glog.Fatalf("Failed to create new RPM file: %s", err) - } - - //Special sauce from: https://github.com/coreos/rpm-ostree/pull/3125 - // add a fake provides that signals to rpm-ostree to let this package override files - if packageCanOverride { - packedRPM.RPMMetaData.Provides = append(packedRPM.RPMMetaData.Provides, &rpmpack.Relation{Name: "rpmostree(override)"}) - } - /*Adding prefixes alone is not enough to make it relocatable (nope, doesn't work) magic numbers come from rpmtag.h packedRPM.AddCustomTag(1098, rpmpack.EntryStringSlice([]string{"/etc/", "/var/", "usr"})) */ - // have to process the envelope if it's machineconfig, otherwise we don't - switch c := configi.(type) { - case ign3types.Config: - - //process the ignition into an RPM - err = Ign2Rpm(packedRPM, &c) - if err != nil { - glog.Fatalf("Unable to convert ignition to RPM payload: %v", err) - } - case mcfgv1.MachineConfig: + ignConfig, err := processEnvelope(configi, &o) + if err != nil { + glog.Fatalf("Unable to get ignition config from %s, check inputs: %v", o.config, err) + } - //shuck the ignition out of the machineconfig if need be - //TODO: fish the name out of the machineconfig for the metadata? - newIgnConfig, err := ctrlcommon.ParseAndConvertConfig(c.Spec.Config.Raw) - if err != nil { - glog.Fatalf("Unable to convert machine config to ignition: %v", err) - } - err = Ign2Rpm(packedRPM, &newIgnConfig) - if err != nil { - glog.Fatalf("Unable to convert ignition to RPM payload: %v", err) - } + rpm, err := NewRPMFromIgnition(o, *ignConfig) + if err != nil { + glog.Fatalf("Unable to convert ignition to RPM payload: %v", err) } //make the rpmfile we're going to write to disk - f, err := os.Create(outputRPM) + f, err := os.Create(o.outputRPM) if err != nil { panic(err) } - //stuff our RPM file guts into it - if err := packedRPM.Write(f); err != nil { - glog.Fatalf("Unable to write RPM %s to disk: %v", outputRPM, err) - } - - glog.Infof("Wrote %s", outputRPM) - -} - -type onceFromOrigin int - -const ( - onceFromUnknownConfig onceFromOrigin = iota - onceFromLocalConfig - onceFromRemoteConfig -) - -// blatantly stolen from mco daemon's once-from -func senseAndLoadOnceFrom(onceFrom string) (interface{}, onceFromOrigin, error) { - var ( - content []byte - contentFrom onceFromOrigin - ) - // Read the content from a remote endpoint if requested - /* #nosec */ - if strings.HasPrefix(onceFrom, "http://") || strings.HasPrefix(onceFrom, "https://") { - contentFrom = onceFromRemoteConfig - resp, err := http.Get(onceFrom) - if err != nil { - return nil, contentFrom, err - } - defer resp.Body.Close() - // Read the body content from the request - content, err = ioutil.ReadAll(resp.Body) - if err != nil { - return nil, contentFrom, err - } - - } else { - // Otherwise read it from a local file - contentFrom = onceFromLocalConfig - absoluteOnceFrom, err := filepath.Abs(filepath.Clean(onceFrom)) - if err != nil { - return nil, contentFrom, err - } - content, err = ioutil.ReadFile(absoluteOnceFrom) - if err != nil { - return nil, contentFrom, err - } - } - - // Try each supported parser - ignConfig, err := ctrlcommon.ParseAndConvertConfig(content) - if err == nil && ignConfig.Ignition.Version != "" { - glog.V(2).Info("onceFrom file is of type Ignition") - return ignConfig, contentFrom, nil - } - - // Try to parse as a machine config - mc, err := resourceread.ReadMachineConfigV1(content) - if err == nil && mc != nil { - glog.V(2).Info("onceFrom file is of type MachineConfig") - return *mc, contentFrom, nil - } - - return nil, onceFromUnknownConfig, fmt.Errorf("unable to decipher onceFrom config type: %v", err) -} - -// makes sure we always get a uint value back, even if nil -func NilMode(obj *int, val uint) uint { - if obj == nil { - return val - } - - octalstring := strconv.FormatInt(int64(*obj), 8) - octal, _ := strconv.ParseInt(octalstring, 8, 64) - return uint(octal) - -} - -// makes sure we always get a string value back, even if nil -func NilString(obj *string, val string) string { - if obj == nil { - return val - } - return *obj -} - -// makes sure we always get a bool value back, even if nil -func NilBool(obj *bool, val bool) bool { - if obj == nil { - return val - } - return *obj -} - -// Rewrites file paths for rpm-ostree so they get linked back into the right place. -// This will break the package though for things like RHEL, because it won't be able to find its toys -func RelocateForRpmOstree(fileName string) string { - prefixes := map[string]string{"/usr/local/": "/var/usrlocal/"} - - // for each prefix in the list - for prefix, target := range prefixes { - - // if our file starts with that prefix - if strings.HasPrefix(fileName, prefix) { - - //replace the prefix with the targer prefix - replaced := strings.Replace(fileName, prefix, target, 1) - glog.Infof("REPLACING: %s %s", fileName, replaced) - return replaced - - } - } - - return fileName - -} - -// Converts ignition to an RPM and returns the RPM -func Ign2Rpm(r *rpmpack.RPM, config *ign3types.Config) error { - - packTime := time.Now().Unix() - - // MCO currently support sshkeys - // yes I'm cheating because I know /var/home becomes /home - coreUserSSHDir := "/var/home/core/.ssh" - for _, u := range config.Passwd.Users { - concatKeys := "" - if u.Name == "core" { - glog.Infof("Found the core user, adding authorized_keys") - for _, key := range u.SSHAuthorizedKeys { - concatKeys = concatKeys + string(key) + "\n" - } - - } - rpmfile := rpmpack.RPMFile{ - - Name: filepath.Join(coreUserSSHDir, "authorized_keys"), - Body: []byte(concatKeys), - Mode: 0644, - Owner: u.Name, - Group: u.Name, - MTime: uint32(packTime), - Type: rpmpack.GenericFile, - } - r.AddFile(rpmfile) - - } - - // TODO: I've never tested directories, don't trust it! - for _, d := range config.Storage.Directories { - glog.Infof("DIR: %s (%d %s) (%d %s) %v\n", d.Path, d.User.ID, d.User.Name, d.Group.ID, d.Group.Name, d.Node) - - // rpm-ostree limits where we can put files - // but it links some of them back into the right spots if we put them where it wants them - d.Path = RelocateForRpmOstree(d.Path) - - rpmfile := rpmpack.RPMFile{ - Name: d.Path, - //Body: []byte(*d.Contents.Source), - // The Nil functions insulate us from nil pointers and return a default value - Mode: NilMode(d.Mode, 0755), - Owner: NilString(d.User.Name, "root"), - Group: NilString(d.Group.Name, "root"), - MTime: uint32(packTime), - Type: rpmpack.GenericFile, - } - //tell the rpm library it's a directory. Yes, this could be better - rpmfile.Mode |= 040000 - r.AddFile(rpmfile) - - } - - // Process the files (this works) - for _, f := range config.Storage.Files { - - if excludePrefix != "" && strings.HasPrefix(f.Path, excludePrefix) { - glog.Infof("SKIPPING (prefix): %s\n", f.Path) - continue - } else { - glog.Infof("FILE: %s\n", f.Path) - } - - var contents *dataurl.DataURL - - if f.Contents.Source != nil { - var err error - contents, err = dataurl.DecodeString(*f.Contents.Source) - if err != nil { - return err - } - } - - //rpm-ostree limits where we can put files - //but it links some of them back into the right spots - f.Path = RelocateForRpmOstree(f.Path) - rpmfile := rpmpack.RPMFile{ - Name: f.Path, - //Body: []byte(*f.Contents.Source), - Body: []byte(contents.Data), - Mode: NilMode(f.Mode, 0755), - Owner: NilString(f.User.Name, "root"), - Group: NilString(f.Group.Name, "root"), - MTime: uint32(packTime), - Type: rpmpack.GenericFile, - } - r.AddFile(rpmfile) - } - - //TODO: I've never tested links, don't trust it ! - for _, l := range config.Storage.Links { - glog.Infof("LINK: %s %s\n", l.Path, l.Node.Path) - - // rpm-ostree limits where we can put files - // but it links some of them back into the right spots if we put them where it wants them - l.Path = RelocateForRpmOstree(l.Path) - rpmfile := rpmpack.RPMFile{ - Name: l.Node.Path, - Body: []byte(l.Path), - Mode: 0755, - Owner: NilString(l.User.Name, "root"), - Group: NilString(l.Group.Name, "root"), - MTime: uint32(packTime), - Type: rpmpack.GenericFile, - } - // magic "this is a link" mode - rpmfile.Mode |= 00120000 - r.AddFile(rpmfile) - - } - - //Loop through the units, put them in the right spot (this also works) - for _, u := range config.Systemd.Units { - unitFile := filepath.Join("/", SystemdUnitsPath(), u.Name) - - glog.Infof("UNIT: %s %s %t\n", u.Name, unitFile, NilBool(u.Enabled, true)) - rpmfile := rpmpack.RPMFile{ - Name: unitFile, - Body: []byte(NilString(u.Contents, "")), - Mode: 0644, - Owner: "root", - Group: "root", - MTime: uint32(packTime), - Type: rpmpack.GenericFile, - } - r.AddFile(rpmfile) - - //Some of these units may have dropins - for _, dropin := range u.Dropins { - dropinFile := filepath.Join("/", SystemdDropinsPath(u.Name), dropin.Name) - glog.Infof("\tDROPIN: %s %s\n", dropin.Name, dropinFile) - rpmfile := rpmpack.RPMFile{ - Name: dropinFile, - Body: []byte(NilString(dropin.Contents, "")), - Mode: 0644, - Owner: "root", - Group: "root", - MTime: uint32(packTime), - Type: rpmpack.GenericFile, - } - r.AddFile(rpmfile) - } + defer f.Close() + //stuff our RPM file guts into it + if err := rpm.Write(f); err != nil { + glog.Fatalf("Unable to write RPM %s to disk: %v", o.outputRPM, err) } - return nil - -} - -// blatantly stolen from ignition internal libraries -func SystemdUnitsPath() string { - return filepath.Join("etc", "systemd", "system") -} - -func SystemdRuntimeUnitsPath() string { - return filepath.Join("run", "systemd", "system") -} - -func SystemdRuntimeUnitWantsPath(unitName string) string { - return filepath.Join("run", "systemd", "system", unitName+".wants") -} - -func SystemdDropinsPath(unitName string) string { - return filepath.Join("etc", "systemd", "system", unitName+".d") -} - -func SystemdRuntimeDropinsPath(unitName string) string { - return filepath.Join("run", "systemd", "system", unitName+".d") + glog.Infof("Wrote %s", o.outputRPM) } diff --git a/cmd/ignition2rpm/rpm.go b/cmd/ignition2rpm/rpm.go new file mode 100644 index 0000000..9f47984 --- /dev/null +++ b/cmd/ignition2rpm/rpm.go @@ -0,0 +1,302 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/vincent-petithory/dataurl" + + ign3types "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/golang/glog" + "github.com/google/rpmpack" +) + +const ( + rootUsername string = "root" + coreUsername string = "core" +) + +type RPMPacker struct { + packedRPM *rpmpack.RPM + packTime uint32 + sshKeys []string +} + +func NewRPMFromIgnition(o opts, config ign3types.Config) (*rpmpack.RPM, error) { + r, err := newRPMPacker(o) + if err != nil { + return nil, fmt.Errorf("could not create RPM: %w", err) + } + + for _, u := range config.Passwd.Users { + // Like the MCO, we only support the default core user + if u.Name == coreUsername { + glog.Infof("Found the core user, adding authorized_keys") + for _, key := range u.SSHAuthorizedKeys { + r.AddSSHKey(key) + } + } + } + + // TODO: I've never tested directories, don't trust it! + for _, d := range config.Storage.Directories { + r.AddDirectory(d) + } + + // Process the files (this works) + for _, f := range config.Storage.Files { + if o.isExcluded(f.Path) { + glog.Infof("SKIPPING (prefix): %s\n", f.Path) + continue + } else { + glog.Infof("FILE: %s\n", f.Path) + if err := r.AddFile(f); err != nil { + return nil, fmt.Errorf("could not add file to RPM: %w", err) + } + } + } + + //TODO: I've never tested links, don't trust it ! + for _, l := range config.Storage.Links { + r.AddLink(l) + } + + //Loop through the units, put them in the right spot (this also works) + for _, u := range config.Systemd.Units { + r.AddUnit(u) + } + + return r.Pack(), nil +} + +func newRPMPacker(o opts) (*RPMPacker, error) { + hostname, err := os.Hostname() + if err != nil { + return nil, err + } + + metadata := rpmpack.RPMMetaData{ + Name: o.packageName(), + Version: "1", + Release: "1", + Summary: "A package packed from " + o.config, + Description: "This is a machine-packed RPM that has been packed by 'ignition2rpm'", + BuildTime: time.Now(), + Packager: "MCO ignition2rpm", + Vendor: "RedHat OpenShift", + //Licence: "", + BuildHost: hostname, + } + + pRPM, err := rpmpack.NewRPM(metadata) + if err != nil { + return nil, err + } + + //Special sauce from: https://github.com/coreos/rpm-ostree/pull/3125 + // add a fake provides that signals to rpm-ostree to let this package override files + if o.packageCanOverride { + pRPM.RPMMetaData.Provides = append(pRPM.RPMMetaData.Provides, &rpmpack.Relation{Name: "rpmostree(override)"}) + } + + return &RPMPacker{ + packedRPM: pRPM, + sshKeys: []string{}, + packTime: uint32(metadata.BuildTime.Unix()), + }, nil +} + +func (r *RPMPacker) AddSSHKey(key ign3types.SSHAuthorizedKey) { + r.sshKeys = append(r.sshKeys, string(key)) +} + +func (r *RPMPacker) AddFile(f ign3types.File) error { + var contents *dataurl.DataURL + + if f.Contents.Source != nil { + var err error + contents, err = dataurl.DecodeString(*f.Contents.Source) + if err != nil { + return err + } + } + + r.addRPMFile(rpmpack.RPMFile{ + Name: RelocateForRpmOstree(f.Path), + //Body: []byte(*f.Contents.Source), + Body: []byte(contents.Data), + Mode: NilMode(f.Mode, 0755), + Owner: NilString(f.User.Name, rootUsername), + Group: NilString(f.Group.Name, rootUsername), + }) + + return nil +} + +func (r *RPMPacker) AddDirectory(d ign3types.Directory) { + glog.Infof("DIR: %s (%d %s) (%d %s) %v\n", d.Path, d.User.ID, d.User.Name, d.Group.ID, d.Group.Name, d.Node) + + rpmfile := rpmpack.RPMFile{ + Name: RelocateForRpmOstree(d.Path), + //Body: []byte(*d.Contents.Source), + // The Nil functions insulate us from nil pointers and return a default value + Mode: NilMode(d.Mode, 0755), + Owner: NilString(d.User.Name, "root"), + Group: NilString(d.Group.Name, "root"), + } + + //tell the rpm library it's a directory. Yes, this could be better + rpmfile.Mode |= 040000 + + r.addRPMFile(rpmfile) +} + +func (r *RPMPacker) AddLink(l ign3types.Link) { + glog.Infof("LINK: %s %s\n", l.Path, l.Node.Path) + + // rpm-ostree limits where we can put files + // but it links some of them back into the right spots if we put them where it wants them + l.Path = RelocateForRpmOstree(l.Path) + rpmfile := rpmpack.RPMFile{ + Name: l.Node.Path, + Body: []byte(l.Path), + Mode: 0755, + Owner: NilString(l.User.Name, "root"), + Group: NilString(l.Group.Name, "root"), + } + + // magic "this is a link" mode + rpmfile.Mode |= 00120000 + + r.addRPMFile(rpmfile) +} + +func (r *RPMPacker) AddUnit(u ign3types.Unit) { + unitFile := filepath.Join("/", SystemdUnitsPath(), u.Name) + + glog.Infof("UNIT: %s %s %t\n", u.Name, unitFile, NilBool(u.Enabled, true)) + r.addRPMFile(rpmpack.RPMFile{ + Name: unitFile, + Body: []byte(NilString(u.Contents, "")), + Mode: 0644, + }) + + //Some of these units may have dropins + for _, dropin := range u.Dropins { + dropinFile := filepath.Join("/", SystemdDropinsPath(u.Name), dropin.Name) + glog.Infof("\tDROPIN: %s %s\n", dropin.Name, dropinFile) + r.addRPMFile(rpmpack.RPMFile{ + Name: dropinFile, + Body: []byte(NilString(dropin.Contents, "")), + Mode: 0644, + }) + } +} + +func (r *RPMPacker) Pack() *rpmpack.RPM { + if len(r.sshKeys) > 0 { + r.addRPMFile(r.sshKeysToRPMFile()) + } + + return r.packedRPM +} + +func (r *RPMPacker) addRPMFile(rpmFile rpmpack.RPMFile) { + rpmFile.MTime = r.packTime + rpmFile.Type = rpmpack.GenericFile + + r.packedRPM.AddFile(rpmFile) +} + +func (r *RPMPacker) sshKeysToRPMFile() rpmpack.RPMFile { + out := bytes.NewBuffer([]byte{}) + + for _, key := range r.sshKeys { + fmt.Fprintln(out, key) + } + + return rpmpack.RPMFile{ + // MCO currently support sshkeys + // yes I'm cheating because I know /var/home becomes /home + Name: "/var/home/core/.ssh/authorized_keys", + Body: out.Bytes(), + Mode: 0644, + Owner: coreUsername, + Group: coreUsername, + } +} + +// makes sure we always get a uint value back, even if nil +func NilMode(obj *int, val uint) uint { + if obj == nil { + return val + } + + octalstring := strconv.FormatInt(int64(*obj), 8) + octal, _ := strconv.ParseInt(octalstring, 8, 64) + return uint(octal) +} + +// makes sure we always get a string value back, even if nil +func NilString(obj *string, val string) string { + if obj == nil { + return val + } + return *obj +} + +// makes sure we always get a bool value back, even if nil +func NilBool(obj *bool, val bool) bool { + if obj == nil { + return val + } + return *obj +} + +// Rewrites file paths for rpm-ostree so they get linked back into the right place. +// This will break the package though for things like RHEL, because it won't be able to find its toys +func RelocateForRpmOstree(fileName string) string { + prefixes := map[string]string{"/usr/local/": "/var/usrlocal/"} + + // for each prefix in the list + for prefix, target := range prefixes { + + // if our file starts with that prefix + if strings.HasPrefix(fileName, prefix) { + + //replace the prefix with the targer prefix + replaced := strings.Replace(fileName, prefix, target, 1) + glog.Infof("REPLACING: %s %s", fileName, replaced) + return replaced + + } + } + + return fileName +} + +// blatantly stolen from ignition internal libraries +func SystemdUnitsPath() string { + return filepath.Join("etc", "systemd", "system") +} + +func SystemdRuntimeUnitsPath() string { + return filepath.Join("run", "systemd", "system") +} + +func SystemdRuntimeUnitWantsPath(unitName string) string { + return filepath.Join("run", "systemd", "system", unitName+".wants") +} + +func SystemdDropinsPath(unitName string) string { + return filepath.Join("etc", "systemd", "system", unitName+".d") +} + +func SystemdRuntimeDropinsPath(unitName string) string { + return filepath.Join("run", "systemd", "system", unitName+".d") +} diff --git a/go.mod b/go.mod index acdedf7..462da87 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,12 @@ module github.com/coreos/mcbs-hackweek go 1.16 require ( + github.com/coreos/butane v0.13.1 github.com/coreos/ignition/v2 v2.12.0 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/google/rpmpack v0.0.0-20210518075352-dc539ef4f2ea github.com/openshift/machine-config-operator v0.0.1-0.20210908062820-e9a580a71623 + github.com/vincent-petithory/dataurl v0.0.0-20160330182126-9a301d65acbb + gopkg.in/yaml.v2 v2.4.0 + k8s.io/apimachinery v0.22.0-rc.0 ) diff --git a/go.sum b/go.sum index 36ba9c6..5b5ede3 100644 --- a/go.sum +++ b/go.sum @@ -253,6 +253,8 @@ github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B github.com/containers/storage v1.28.1/go.mod h1:5bwiMh2LkrN3AWIfDFMH7A/xbVNLcve+oeXYvHvW8cc= github.com/containers/storage v1.32.6/go.mod h1:mdB+b89p+jU8zpzLTVXA0gWMmIo0WrkfGMh1R8O2IQw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/butane v0.13.1 h1:VN7HcxBaqJAMTqJQn/RFAYzfjsv8LOOBjDYbhSCJzsY= +github.com/coreos/butane v0.13.1/go.mod h1:m8mELrooVMftGW5uqHOfiwcBXvLG3XBch934zV2PhWY= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/fcct v0.5.0 h1:f/z+MCoR2vULes+MyoPEApQ6iluy/JbXoRi6dahPItQ= @@ -282,6 +284,7 @@ github.com/coreos/ignition v0.35.0 h1:UFodoYq1mOPrbEjtxIsZbThcDyQwAI1owczRDqWmKk github.com/coreos/ignition v0.35.0/go.mod h1:WJQapxzEn9DE0ryxsGvm8QnBajm/XsS/PkrDqSpz+bA= github.com/coreos/ignition/v2 v2.1.1/go.mod h1:RqmqU64zxarUJa3l4cHtbhcSwfQLpUhv0WVziZwoXvE= github.com/coreos/ignition/v2 v2.7.0/go.mod h1:3CjaRpg51hmJzPjarbzB0RvSZbLkNOczxKJobTl6nOY= +github.com/coreos/ignition/v2 v2.11.0/go.mod h1:uFhfdmeUfzT/8MqBvazzrLdzR3DvMCWR78GUYFRwPrs= github.com/coreos/ignition/v2 v2.12.0 h1:JK9SI80URnoMah7m5oZndBoyED2TWr5ntrwxRItl1zY= github.com/coreos/ignition/v2 v2.12.0/go.mod h1:PEKv4yQSfWLya4y6vY1K+7+2lOf9t7O7gGljmp0hLIg= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -930,8 +933,9 @@ github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzu github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace h1:9PNP1jnUjRhfmGMlkXHjYPishpcw4jpSt/V/xYY3FMA= +github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=