mirror of
https://github.com/coreos/ignition.git
synced 2026-02-06 00:47:49 +01:00
At this point, the file has been created without error, so the execution can continue and the logic will not change if we ignore this error. Failure here seams unlikely. Fixes lint: ``` Error return value of `f.Close` is not checked (errcheck) ```
345 lines
11 KiB
Go
345 lines
11 KiB
Go
// Copyright 2015 CoreOS, Inc.
|
|
//
|
|
// 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 exec
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/coreos/go-systemd/v22/journal"
|
|
"github.com/coreos/ignition/v2/config/shared/errors"
|
|
latest "github.com/coreos/ignition/v2/config/v3_6_experimental"
|
|
"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
|
|
"github.com/coreos/ignition/v2/internal/exec/stages"
|
|
executil "github.com/coreos/ignition/v2/internal/exec/util"
|
|
"github.com/coreos/ignition/v2/internal/log"
|
|
"github.com/coreos/ignition/v2/internal/platform"
|
|
"github.com/coreos/ignition/v2/internal/providers/cmdline"
|
|
"github.com/coreos/ignition/v2/internal/providers/system"
|
|
"github.com/coreos/ignition/v2/internal/resource"
|
|
"github.com/coreos/ignition/v2/internal/state"
|
|
|
|
"github.com/coreos/vcontext/report"
|
|
"github.com/coreos/vcontext/validate"
|
|
"github.com/google/renameio/v2"
|
|
)
|
|
|
|
const (
|
|
DefaultFetchTimeout = 2 * time.Minute
|
|
// This variable will help to identify ignition journal messages
|
|
// related to the user/base config.
|
|
ignitionFetchedConfigMsgId = "57124006b5c94805b77ce473e92a8aeb"
|
|
)
|
|
|
|
var (
|
|
emptyConfig = types.Config{
|
|
Ignition: types.Ignition{Version: types.MaxVersion.String()},
|
|
}
|
|
)
|
|
|
|
// Engine represents the entity that fetches and executes a configuration.
|
|
type Engine struct {
|
|
ConfigCache string
|
|
FetchTimeout time.Duration
|
|
Logger *log.Logger
|
|
NeedNet string
|
|
Root string
|
|
PlatformConfig platform.Config
|
|
Fetcher *resource.Fetcher
|
|
State *state.State
|
|
}
|
|
|
|
// Run executes the stage of the given name. It returns true if the stage
|
|
// successfully ran and false if there were any errors.
|
|
func (e Engine) Run(stageName string) error {
|
|
if e.Fetcher == nil || e.Logger == nil {
|
|
fmt.Fprintf(os.Stderr, "engine incorrectly configured\n")
|
|
return errors.ErrEngineConfiguration
|
|
}
|
|
baseConfig := emptyConfig
|
|
|
|
systemBaseConfig, r, err := system.FetchBaseConfig(e.Logger, e.PlatformConfig.Name())
|
|
e.Logger.LogReport(r)
|
|
if err != nil && err != platform.ErrNoProvider {
|
|
e.Logger.Crit("failed to acquire system base config: %v", err)
|
|
return err
|
|
} else if err == nil {
|
|
e.State.FetchedConfigs = append(e.State.FetchedConfigs, state.FetchedConfig{
|
|
Kind: "base",
|
|
Source: "system",
|
|
Referenced: false,
|
|
})
|
|
}
|
|
|
|
// We special-case the fetch-offline stage a bit here: we want to be able
|
|
// to handle the case where the provider itself requires networking.
|
|
if stageName == "fetch-offline" {
|
|
e.Fetcher.Offline = true
|
|
}
|
|
|
|
// Run the platform config's Init function pre-config fetch
|
|
// to perform any additional fetcher configuration e.x.
|
|
// configuring the S3RegionHint when running on AWS.
|
|
err = e.PlatformConfig.Init(e.Fetcher)
|
|
if err == resource.ErrNeedNet && stageName == "fetch-offline" {
|
|
err = e.signalNeedNet()
|
|
if err != nil {
|
|
e.Logger.Crit("failed to signal neednet: %v", err)
|
|
}
|
|
return err
|
|
} else if err != nil {
|
|
return fmt.Errorf("initializing platform config: %v", err)
|
|
}
|
|
|
|
cfg, err := e.acquireConfig(stageName)
|
|
if err == resource.ErrNeedNet && stageName == "fetch-offline" {
|
|
err = e.signalNeedNet()
|
|
if err != nil {
|
|
e.Logger.Crit("failed to signal neednet: %v", err)
|
|
}
|
|
return err
|
|
} else if err == errors.ErrEmpty {
|
|
e.Logger.Info("%v: ignoring user-provided config", err)
|
|
} else if err != nil {
|
|
e.Logger.Crit("failed to acquire config: %v", err)
|
|
return err
|
|
}
|
|
|
|
e.Logger.PushPrefix("%s", stageName)
|
|
defer e.Logger.PopPrefix()
|
|
|
|
fullConfig := latest.Merge(baseConfig, latest.Merge(systemBaseConfig, cfg))
|
|
err = stages.Get(stageName).Create(e.Logger, e.Root, *e.Fetcher, e.State).Run(fullConfig)
|
|
if err == resource.ErrNeedNet && stageName == "fetch-offline" {
|
|
err = e.signalNeedNet()
|
|
if err != nil {
|
|
e.Logger.Crit("failed to signal neednet: %v", err)
|
|
}
|
|
// fall through
|
|
}
|
|
if err != nil {
|
|
// e.Logger could be nil
|
|
fmt.Fprintf(os.Stderr, "%s failed\n", stageName)
|
|
tmp, jsonerr := json.MarshalIndent(fullConfig, "", " ")
|
|
if jsonerr != nil {
|
|
// Nothing else to do with this error
|
|
fmt.Fprintf(os.Stderr, "Could not marshal full config: %v\n", jsonerr)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Full config:\n%s\n", string(tmp))
|
|
}
|
|
return err
|
|
}
|
|
e.Logger.Info("%s passed", stageName)
|
|
return nil
|
|
}
|
|
|
|
// logStructuredJournalEntry logs information related to
|
|
// a user/base config into the systemd journal log.
|
|
func logStructuredJournalEntry(cfgInfo state.FetchedConfig) error {
|
|
ignitionInfo := map[string]string{
|
|
"IGNITION_CONFIG_TYPE": cfgInfo.Kind,
|
|
"IGNITION_CONFIG_SRC": cfgInfo.Source,
|
|
"IGNITION_CONFIG_REFERENCED": strconv.FormatBool(cfgInfo.Referenced),
|
|
"MESSAGE_ID": ignitionFetchedConfigMsgId,
|
|
}
|
|
referenced := ""
|
|
if cfgInfo.Referenced {
|
|
referenced = "referenced "
|
|
}
|
|
msg := fmt.Sprintf("fetched %s%s config from %q", referenced, cfgInfo.Kind, cfgInfo.Source)
|
|
if err := journal.Send(msg, journal.PriInfo, ignitionInfo); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// acquireConfig will perform differently based on the stage it is being
|
|
// called from. In fetch stages it will attempt to fetch the provider
|
|
// config (writing an empty provider config if it is empty). In all other
|
|
// stages it will attempt to fetch from the local cache only.
|
|
func (e *Engine) acquireConfig(stageName string) (cfg types.Config, err error) {
|
|
switch {
|
|
case strings.HasPrefix(stageName, "fetch"):
|
|
cfg, err = e.acquireProviderConfig()
|
|
|
|
// if we've successfully fetched and cached the configs, log about them
|
|
if err == nil && journal.Enabled() {
|
|
for _, cfgInfo := range e.State.FetchedConfigs {
|
|
if logerr := logStructuredJournalEntry(cfgInfo); logerr != nil {
|
|
e.Logger.Info("failed to log systemd journal entry: %v", logerr)
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
cfg, err = e.acquireCachedConfig()
|
|
}
|
|
return
|
|
}
|
|
|
|
// acquireCachedConfig returns the configuration from a local cache if
|
|
// available
|
|
func (e *Engine) acquireCachedConfig() (cfg types.Config, err error) {
|
|
var b []byte
|
|
b, err = os.ReadFile(e.ConfigCache)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err = json.Unmarshal(b, &cfg); err != nil {
|
|
e.Logger.Crit("failed to parse cached config: %v", err)
|
|
return
|
|
}
|
|
// Create an http client and fetcher with the timeouts from the cached
|
|
// config
|
|
err = e.Fetcher.UpdateHttpTimeoutsAndCAs(cfg.Ignition.Timeouts, cfg.Ignition.Security.TLS.CertificateAuthorities, cfg.Ignition.Proxy)
|
|
if err != nil {
|
|
e.Logger.Crit("failed to update timeouts and CAs for fetcher: %v", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// acquireProviderConfig attempts to fetch the configuration from the
|
|
// provider.
|
|
func (e *Engine) acquireProviderConfig() (cfg types.Config, err error) {
|
|
// Create a new http client and fetcher with the timeouts set via the flags,
|
|
// since we don't have a config with timeout values we can use
|
|
timeout := int(e.FetchTimeout.Seconds())
|
|
emptyProxy := types.Proxy{}
|
|
err = e.Fetcher.UpdateHttpTimeoutsAndCAs(types.Timeouts{HTTPTotal: &timeout}, nil, emptyProxy)
|
|
if err != nil {
|
|
e.Logger.Crit("failed to update timeouts and CAs for fetcher: %v", err)
|
|
return
|
|
}
|
|
|
|
// (Re)Fetch the config if the cache is unreadable.
|
|
cfg, err = e.fetchProviderConfig()
|
|
if err == errors.ErrEmpty {
|
|
// Continue if the provider config was empty as we want to write an empty
|
|
// cache config for use by other stages.
|
|
cfg = emptyConfig
|
|
e.Logger.Info("%v: provider config was empty, continuing with empty cache config", err)
|
|
} else if err == resource.ErrNeedNet {
|
|
e.Logger.Info("failed to fetch config: %s", err)
|
|
return
|
|
} else if err != nil {
|
|
e.Logger.Warning("failed to fetch config: %s", err)
|
|
return
|
|
}
|
|
|
|
// Update the http client to use the timeouts and CAs from the newly fetched
|
|
// config
|
|
err = e.Fetcher.UpdateHttpTimeoutsAndCAs(cfg.Ignition.Timeouts, cfg.Ignition.Security.TLS.CertificateAuthorities, cfg.Ignition.Proxy)
|
|
if err != nil {
|
|
e.Logger.Crit("failed to update timeouts and CAs for fetcher: %v", err)
|
|
return
|
|
}
|
|
|
|
err = e.Fetcher.RewriteCAsWithDataUrls(cfg.Ignition.Security.TLS.CertificateAuthorities)
|
|
if err != nil {
|
|
e.Logger.Crit("error handling CAs: %v", err)
|
|
return
|
|
}
|
|
|
|
rpt := validate.Validate(cfg, "json")
|
|
e.Logger.LogReport(rpt)
|
|
if rpt.IsFatal() {
|
|
err = errors.ErrInvalid
|
|
e.Logger.Crit("merging configs resulted in an invalid config")
|
|
return
|
|
}
|
|
|
|
// Populate the config cache.
|
|
b, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
e.Logger.Crit("failed to marshal cached config: %v", err)
|
|
return
|
|
}
|
|
if err = renameio.WriteFile(e.ConfigCache, b, 0640); err != nil {
|
|
e.Logger.Crit("failed to write cached config: %v", err)
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// fetchProviderConfig returns the externally-provided configuration. It first
|
|
// checks to see if the command-line option is present. If so, it uses that
|
|
// source for the configuration. If the command-line option is not present, it
|
|
// checks for a user config in the system config dir. If that is also missing,
|
|
// it checks the config engine's provider. An error is returned if the provider
|
|
// is unavailable. This will also render the config (see renderConfig) before
|
|
// returning.
|
|
func (e *Engine) fetchProviderConfig() (types.Config, error) {
|
|
platformConfigs := []platform.Config{
|
|
cmdline.Config,
|
|
system.Config,
|
|
e.PlatformConfig,
|
|
}
|
|
var cfg types.Config
|
|
var r report.Report
|
|
var err error
|
|
var providerKey string
|
|
for _, platformConfig := range platformConfigs {
|
|
cfg, r, err = platformConfig.Fetch(e.Fetcher, e.State)
|
|
if err != platform.ErrNoProvider {
|
|
// successful, or failed on another error
|
|
providerKey = platformConfig.Name()
|
|
break
|
|
}
|
|
}
|
|
|
|
e.Logger.LogReport(r)
|
|
if err != nil {
|
|
return types.Config{}, err
|
|
}
|
|
|
|
e.State.FetchedConfigs = append(e.State.FetchedConfigs, state.FetchedConfig{
|
|
Kind: "user",
|
|
Source: providerKey,
|
|
Referenced: false,
|
|
})
|
|
|
|
// Replace the HTTP client in the fetcher to be configured with the
|
|
// timeouts of the config
|
|
err = e.Fetcher.UpdateHttpTimeoutsAndCAs(cfg.Ignition.Timeouts, cfg.Ignition.Security.TLS.CertificateAuthorities, cfg.Ignition.Proxy)
|
|
if err != nil {
|
|
return types.Config{}, err
|
|
}
|
|
|
|
configFetcher := ConfigFetcher{
|
|
Logger: e.Logger,
|
|
Fetcher: e.Fetcher,
|
|
State: e.State,
|
|
}
|
|
|
|
return configFetcher.RenderConfig(cfg)
|
|
}
|
|
|
|
func (e *Engine) signalNeedNet() error {
|
|
if err := executil.MkdirForFile(e.NeedNet); err != nil {
|
|
return err
|
|
}
|
|
if f, err := os.Create(e.NeedNet); err != nil {
|
|
return err
|
|
} else {
|
|
_ = f.Close()
|
|
}
|
|
return nil
|
|
}
|