diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 4f4adde..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Release with goreleaser - -on: - workflow_dispatch: - push: - tags: - - v* - -permissions: write-all # Necessary for the generate-build-provenance action with containers - -jobs: - - build: - - - runs-on: ubuntu-latest - - steps: - - name: Set up Go 1.21 - uses: actions/setup-go@v5 - with: - go-version: 1.21 - - name: Docker Login - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-tags: 1 - fetch-depth: 0 - - name: Release with goreleaser - uses: goreleaser/goreleaser-action@v6 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - version: latest - args: release --clean - id: goreleaser - - name: Attest Binaries - uses: actions/attest-build-provenance@v1 - with: - subject-path: '${{ github.workspace }}/dist/configurator_linux_amd64_v1/configurator' \ No newline at end of file diff --git a/.gitignore b/.gitignore index a51dd2f..e848d1d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ **.conf **.ignore **.tar.gz -dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml deleted file mode 100644 index d91d3e3..0000000 --- a/.goreleaser.yaml +++ /dev/null @@ -1,61 +0,0 @@ -version: 2 - -before: - hooks: - - go mod download - - make plugins -builds: - - id: "configurator" - goos: - - linux - goarch: - - amd64 - - arm64 - flags: - - -tags=all -archives: - - format: tar.gz - # this name template makes the OS and Arch compatible with the results of uname. - name_template: >- - {{ .ProjectName }}_ - {{- title .Os }}_ - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} - files: - - LICENSE - - CHANGELOG.md - - README.md -dockers: - - - image_templates: - - ghcr.io/openchami/{{.ProjectName}}:latest - - ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }} - - ghcr.io/openchami/{{.ProjectName}}:v{{ .Major }} - - ghcr.io/openchami/{{.ProjectName}}:v{{ .Major }}.{{ .Minor }} - build_flag_templates: - - "--pull" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - extra_files: - - LICENSE - - CHANGELOG.md - - README.md -checksum: - name_template: 'checksums.txt' -snapshot: - name_template: "{{ incpatch .Version }}-next" -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' -release: - github: - name_template: "{{.Version}}" - prerelease: auto - mode: append \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a6c68b3..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,10 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -[0.1.0] - -- Initial prerelease of configurator \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3a1e3a4..0000000 --- a/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM cgr.dev/chainguard/wolfi-base - -RUN apk add --no-cache tini bash -RUN mkdir -p /configurator - -# nobody 65534:65534 -USER 65534:65534 - -# copy the binary and all of the default plugins -COPY configurator /configurator/configurator - -CMD ["/configurator/configurator"] - -ENTRYPOINT [ "/sbin/tini", "--" ] \ No newline at end of file diff --git a/LICENSE b/LICENSE.md similarity index 97% rename from LICENSE rename to LICENSE.md index aa18635..a17415c 100644 --- a/LICENSE +++ b/LICENSE.md @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile index b6784ec..f901aa8 100644 --- a/Makefile +++ b/Makefile @@ -1,56 +1,35 @@ -# Unless set otherwise, the container runtime is Docker -DOCKER ?= docker - -prog ?= configurator -git_tag := $(shell git describe --abbrev=0 --tags --always) -sources := main.go $(wildcard cmd/*.go) -plugin_source_prefix := pkg/generator/plugins -plugin_sources := $(filter-out %_test.go,$(wildcard $(plugin_source_prefix)/*/*.go)) -plugin_binaries := $(addprefix lib/,$(patsubst %.go,%.so,$(notdir $(plugin_sources)))) # build everything at once -.PHONY: all all: plugins exe test # build the main executable to make configs -.PHONY: main driver binaries exe main: exe driver: exe -binaries: exe plugins -exe: $(prog) - -# build named executable from go sources -$(prog): $(sources) - go build --tags=all -o $(prog) - -.PHONY: container -container: binaries - $(DOCKER) build . --build-arg --no-cache --pull --tag '$(prog):$(git_tag)-dirty' - -.PHONY: container-testing -container-testing: binaries - $(DOCKER) build . --tag $(prog):testing +exe: + go build --tags=all -o configurator # build all of the generators into plugins -.PHONY: plugins -plugins: $(plugin_binaries) - -# how to make each plugin -lib/%.so: pkg/generator/plugins/%/*.go +plugins: mkdir -p lib - go build -buildmode=plugin -o $@ $< + go build -buildmode=plugin -o lib/conman.so pkg/generator/plugins/conman/conman.go + go build -buildmode=plugin -o lib/coredhcp.so pkg/generator/plugins/coredhcp/coredhcp.go + go build -buildmode=plugin -o lib/dhcpd.so pkg/generator/plugins/dhcpd/dhcpd.go + go build -buildmode=plugin -o lib/dnsmasq.so pkg/generator/plugins/dnsmasq/dnsmasq.go + go build -buildmode=plugin -o lib/example.so pkg/generator/plugins/example/example.go + go build -buildmode=plugin -o lib/hostfile.so pkg/generator/plugins/hostfile/hostfile.go + go build -buildmode=plugin -o lib/powerman.so pkg/generator/plugins/powerman/powerman.go + go build -buildmode=plugin -o lib/syslog.so pkg/generator/plugins/syslog/syslog.go + go build -buildmode=plugin -o lib/warewulf.so pkg/generator/plugins/warewulf/warewulf.go docs: go doc github.com/OpenCHAMI/cmd go doc github.com/OpenCHAMI/pkg/configurator # remove executable and all built plugins -.PHONY: clean clean: - rm -f configurator - rm -f lib/* + rm configurator + rm lib/* # run all of the unit tests -.PHONY: test -test: $(prog) $(plugin_binaries) - go test ./tests/generate_test.go --tags=all \ No newline at end of file +test: + go test ./tests/generate_test.go --tags=all diff --git a/README.md b/README.md index ca94345..3fe72a3 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,91 @@ # OpenCHAMI Configurator -The `configurator` is an extensible tool that is capable of dynamically generating files on the fly. The tool includes a built-in generator that fetchs data from an instance of [SMD](https://github.com/OpenCHAMI/smd) to generate files based on Jinja 2 template files. The tool and generator plugins are written in Go and plugins can be written by following the ["Creating Generator Plugins"](#creating-generator-plugins) section of this README. +The `configurator` (portmanteau of config + generator) is an extensible tool that fetchs data from an instance of [SMD](https://github.com/OpenCHAMI/smd) to generate commonly used config files based on Jinja 2 template files. The tool and generator plugins are written in Go and plugins can be written by following the ["Creating Generator Plugins"](#creating-generator-plugins) section of this README. ## Building and Usage -The `configurator` is built using standard `go` build tools. The project separates the client, server, and generator components using build tags. To get started, clone the project, download the dependencies, and build the project: +The `configurator` is built using standard `go` build tools. The project separates the client and server with build tags. To get started, clone the project, download the dependencies, and build the project: ```bash git clone https://github.com/OpenCHAMI/configurator.git go mod tidy go build --tags all # equivalent to `go build --tags client,server`` + +## ...or just run `make` in project directory ``` -This will build the main driver program with the default generators that are found in the `pkg/generators` directory. - -> [!WARNING] -> Not all of the plugins have completed generation implementations and are a WIP. - -### Running Configurator with CLI - -After you build the program, run the following command to use the tool: +This will build the main driver program, but also requires generator plugins to define how new config files are generated. The default plugins can be built using the following build command: ```bash -export ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIs... -./configurator generate --config config.yaml --target coredhcp -o coredhcp.conf --cacert ochami.pem +go build -buildmode=plugin -o lib/conman.so internal/generator/plugins/conman/conman.go +go build -buildmode=plugin -o lib/coredhcp.so internal/generator/plugins/coredhcp/coredhcp.go +go build -buildmode=plugin -o lib/dnsmasq.so internal/generator/plugins/dnsmasq/dnsmasq.go +go build -buildmode=plugin -o lib/powerman.so internal/generator/plugins/powerman/powerman.go +go build -buildmode=plugin -o lib/syslog.so internal/generator/plugins/syslog/syslog.go ``` -This will generate a new `coredhcp` config file based on the Jinja 2 template specified in the config file for "coredhcp". The files will be written to `coredhcp.conf` as specified with the `-o/--output` flag. The `--target` flag specifies the type of config file to generate by its name (see the [`Creating Generator Plugins`](#creating-generator-plugins) section for details). +**NOTE: Not all of the plugins have completed generation implementations and are WIP.** -In other words, there should be an entry in the config file that looks like this: - -```yaml -... -targets: - coredhcp: - plugin: "lib/coredhcp.so" # optional, if we want to use an external plugin instead - templates: - - templates/coredhcp.j2 -... +These commands will build the default plugins and store them in the "lib" directory by default. Alternatively, the plugins can be built using `make plugins` if GNU make is installed and available. After you build the plugins, run the following to use the tool: +```bash +./configurator generate --config config.yaml --target dnsmasq -o dnsmasq.conf ``` -> [!NOTE] -> The `configurator` tool requires a valid access token when making requests to an instance of SMD that has protected routes. - -### Running Configurator as a Service +This will generate a new `dnsmasq` config file based on the Jinja 2 template specified in the config file for "dnsmasq". The `--target` flag specifies the type of config file to generate by its name (see the [`Creating Generator Plugins`](#creating-generator-plugins) section for details). The `configurator` tool requires a valid access token when making requests to an instance of SMD that has protected routes. The tool can also run as a service to generate files for clients: ```bash -export CONFIGURATOR_JWKS_URL="http://my.openchami.cluster:8443/key" ./configurator serve --config config.yaml ``` Once the server is up and listening for HTTP requests, you can try making a request to it with `curl` or `configurator fetch`. Both commands below are essentially equivalent: ```bash -export ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIs... -curl http://127.0.0.1:3334/generate?target=coredhcp -X GET -H "Authorization: Bearer $ACCESS_TOKEN" --cacert ochami.pem +curl http://127.0.0.1:3334/generate?target=dnsmasq -H "Authorization: Bearer $ACCESS_TOKEN" # ...or... -./configurator fetch --target coredhcp --host http://127.0.0.1:3334 --cacert ochami.pem +./configurator fetch --target dnsmasq --host http://127.0.0.1 --port 3334 ``` -This will do the same thing as the `generate` subcommand, but through a GET request where the file contents is returned in the response. The access token is only required if the `CONFIGURATOR_JWKS_URL` environment variable is set when starting the server with `serve`. The `ACCESS_TOKEN` environment variable is passed to `curl` using the `Authorization` header and expects a token as a JWT. - -### Docker - -New images can be built and tested using the `Dockerfile` provided in the project. However, the binary executable and the generator plugins must first be built before building the image since the Docker build copies the binary over. Therefore, build all of the binaries first by following the first section of ["Building and Usage"](#building-and-usage). Running `make docker` from the Makefile will automate this process. Otherwise, run the `docker build` command after building the executable and libraries. - -```bash -docker build -t configurator:testing path/to/configurator/Dockerfile -``` - -If you want to easily include your own external generator plugins, you can build it and copy the `lib.so` file to `lib/`. Make sure that the `Generator` interface is implemented correctly as described in the ["Creating Generator Plugins"](#creating-generator-plugins) or the plugin will not load (you should get an error that specifically says this). Additionally, the name string returned from the `GetName()` method is used for looking up the plugin with the `--target` flag by the main driver program. - -Alternatively, pull the latest existing image/container from the GitHub container repository. - -```bash -docker pull ghcr.io/openchami/configurator:latest -``` - -Then, run the Docker container similarly to running the binary. - -```bash -export ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIs... -docker run ghcr.io/openchami/configurator:latest configurator generate --config config.yaml --target coredhcp -o coredhcp.conf --cacert configurator.pem -``` +This will do the same thing as the `generate` subcommand, but remotely. The access token is only required if the `CONFIGURATOR_JWKS_URL` environment variable is set. The `ACCESS_TOKEN` environment variable passed to `curl` and it's corresponding CLI argument both expects a token as a JWT. ### Creating Generator Plugins -The `configurator` uses built-in and user-defined generators that implement the `Generator` interface to describe how config files should be generated. The interface is defined like so: +The `configurator` uses generator plugins to define how config files are generated using a `Generator` interface. The interface is defined like so: ```go -// maps the file path to its contents -type FileMap = map[string][]byte - -// interface for generator plugins +type Files = map[string][]byte type Generator interface { GetName() string GetVersion() string GetDescription() string - Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) + Generate(config *configurator.Config, opts ...util.Option) (Files, error) } ``` -A new plugin can be created by implementing the methods from interface and exporting a symbol with `Generator` as the name and the plugin struct as the type. The `GetName()` function returns the name that is used for looking up the corresponding target set in your config file. It can also be included in the templated files with the default plugins using the `{{ plugin_name }}` in your template. The `GetVersion()` and `GetDescription()` functions returns the version and description of the plugin which can be included in the templated files using `{{ plugin_version }}` and `{{ plugin_description }}` respectively with the default plugins. The `Generate` function is where the magic happens to build the config file from a template. +A new plugin can be created by implementing the methods from interface and exporting a symbol with `Generator` as the name and the plugin struct as the type. The `GetName()` function returns the name that is used for looking up the corresponding template set in your config file. It can also be included in the templated files with the default plugins using the `{{ plugin_name }}` in your template. The `GetVersion()` and `GetDescription()` functions returns the version and description of the plugin which can be included in the templated files using `{{ plugin_version }}` and `{{ plugin_description }}` respectively with the default plugins. The `Generate` function is where the magic happens to build the config file from a template. ```go package main -type MyGenerator struct { - PluginInfo map[string]any -} - -var pluginInfo map[string]any - -// this function is not a part of the `Generator` interface -func (g *MyGenerator) LoadFromFile() map[string]any{ /*...*/ } +type MyGenerator struct {} func (g *MyGenerator) GetName() string { // just an example...this can be done however you want - g.PluginInfo := LoadFromFile("path/to/plugin/info.json") - return g.PluginInfo["name"] + pluginInfo := LoadFromFile("path/to/plugin/info.json") + return pluginInfo["name"] } func (g *MyGenerator) GetVersion() string { - return g.PluginInfo["version"] // "v1.0.0" + return "v1.0.0" } func (g *MyGenerator) GetDescription() string { - return g.PluginInfo["description"] // "This is an example plugin." + return "This is an example plugin." } -func (g *MyGenerator) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { +func (g *MyGenerator) Generate(config *configurator.Config, opts ...util.Option) (generator.Files, error) { // do config generation stuff here... var ( params = generator.GetParams(opts...) @@ -142,35 +95,28 @@ func (g *MyGenerator) Generate(config *configurator.Config, opts ...util.Option) if client { eths, err := client.FetchEthernetInterfaces(opts...) // ... blah, blah, blah, check error, format output, and so on... - - - // apply the substitutions to Jinja template and return output as FileMap (i.e. path and it's contents) - return generator.ApplyTemplate(path, generator.Mappings{ - "plugin_name": g.GetName(), - "plugin_version": g.GetVersion(), - "plugin_description": g.GetDescription(), - "dhcp_hosts": output, - }) } + + // apply the substitutions to Jinja template and return output as byte array + return generator.ApplyTemplate(path, generator.Mappings{ + "plugin_name": g.GetName(), + "plugin_version": g.GetVersion(), + "plugin_description": g.GetDescription(), + "output": output, + }) } // this MUST be named "Generator" for symbol lookup in main driver var Generator MyGenerator ``` -> [!NOTE] -> The keys in `generator.ApplyTemplate` must not contain illegal characters such as a `-` or else the templates will not apply correctly. - Finally, build the plugin and put it somewhere specified by `plugins` in your config. Make sure that the package is `main` before building. ```bash go build -buildmode=plugin -o lib/mygenerator.so path/to/mygenerator.go ``` -Now your plugin should be available to use with the `configurator` main driver program. If you get an error about not loading the correct symbol type, make sure that your generator function definitions match the `Generator` interface entirely and that you don't have a partially implemented interface. - -> [!TIP] -> See the `examples/test.go` file for a plugin and template example. +Now your plugin should be available to use with the `configurator` main driver. If you get an error about not loading the correct symbol type, make sure that you generator function definitions match the `Generator` interface exactly. ## Configuration @@ -180,33 +126,46 @@ Here is an example config file to start using configurator: server: # Server-related parameters when using as service host: 127.0.0.1 port: 3334 - jwks: # Set the JWKS uri for protected routes + jwks: # Set the JWKS uri to protect /generate route uri: "" retries: 5 -smd: # SMD-related parameters - host: http://127.0.0.1:27779 +smd: . # SMD-related parameters + host: http://127.0.0.1 + port: 27779 plugins: # path to plugin directories - "lib/" targets: # targets to call with --target flag - coredhcp: + dnsmasq: templates: - - templates/coredhcp.j2 + - templates/dnsmasq.jinja + warewulf: + templates: # files using Jinja templating + - templates/warewulf/vnfs/dhcpd-template.jinja + - templates/warewulf/vnfs/dnsmasq-template.jinja files: # files to be copied without templating - - extra/nodes.conf - targets: # additional targets to run (does not run recursively) + - templates/warewulf/defaults/provision.jinja + - templates/warewulf/defaults/node.jinja + - templates/warewulf/filesystem/examples/* + - templates/warewulf/vnfs/* + - templates/warewulf/bootstrap.jinja + - templates/warewulf/database.jinja + targets: # additional targets to run - dnsmasq ``` -The `server` section sets the properties for running the `configurator` tool as a service and is not required if you're only using the CLI. Also note that the `jwks.uri` parameter is only needed for protecting endpoints. If it is not set, then all API routes are entirely public. The `smd` section tells the `configurator` tool where to find the SMD service to pull state management data used internally by the client's generator. The `templates` section is where the paths are mapped to each generator by its name (see the [`Creating Generator Plugins`](#creating-generator-plugins) section for details). The `plugins` is a list of paths to search for and load external generator plugins. +The `server` section sets the properties for running the `configurator` tool as a service and is not required if you're only using the CLI. Also note that the `jwks-uri` parameter is only needs for protecting endpoints. If it is not set, then the API is entirely public. The `smd` section tells the `configurator` tool where to find SMD to pull state management data used by the internal client. The `templates` section is where the paths are mapped to each generator plugin by its name (see the [`Creating Generator Plugins`](#creating-generator-plugins) section for details). The `plugins` is a list of paths to load generator plugins. ## Running the Tests -The `configurator` project includes a collection of tests focused on verifying plugin behavior and generating files. The tests do not include fetching information from any remote sources, can be ran with the following command: +The `configurator` project includes a collection of tests focused on verifying plugin behavior and generating files. The tests do not currently test fetching information from SMD (or whatever remote source). The tests can be ran with either of the following commands: ```bash go test ./tests/generate_test.go --tags=all +# ...or alternatively with GNU make... +make test ``` + ## Known Issues - Adds a new `OAuthClient` with every token request diff --git a/cmd/config.go b/cmd/config.go index f56c34c..1de4015 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -1,10 +1,11 @@ package cmd import ( - "github.com/rs/zerolog/log" + "fmt" + "github.com/spf13/cobra" - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "github.com/OpenCHAMI/configurator/pkg" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -19,10 +20,10 @@ var configCmd = &cobra.Command{ for _, path := range args { // check and make sure something doesn't exist first if exists, err := util.PathExists(path); exists || err != nil { - log.Error().Err(err).Msg("file or directory exists") + fmt.Printf("file or directory exists\n") continue } - config.SaveDefault(path) + configurator.SaveDefaultConfig(path) } }, } diff --git a/cmd/fetch.go b/cmd/fetch.go index 0b105a7..ef7e16e 100644 --- a/cmd/fetch.go +++ b/cmd/fetch.go @@ -13,6 +13,12 @@ import ( "github.com/spf13/cobra" ) +var ( + accessToken string + remoteHost string + remotePort int +) + var fetchCmd = &cobra.Command{ Use: "fetch", Short: "Fetch a config file from a remote instance of configurator", @@ -24,22 +30,16 @@ var fetchCmd = &cobra.Command{ return } - // check if we actually have any targets to run - if len(targets) <= 0 { - log.Error().Msg("must specify a target") - os.Exit(1) - } - // check to see if an access token is available from env - if conf.AccessToken == "" { + if config.AccessToken == "" { // check if OCHAMI_ACCESS_TOKEN env var is set if no access token is provided and use that instead accessToken := os.Getenv("ACCESS_TOKEN") if accessToken != "" { - conf.AccessToken = accessToken + config.AccessToken = accessToken } else { // TODO: try and fetch token first if it is needed if verbose { - log.Warn().Msg("No token found. Attempting to generate config without one...") + fmt.Printf("No token found. Attempting to generate config without one...\n") } } } @@ -52,23 +52,25 @@ var fetchCmd = &cobra.Command{ for _, target := range targets { // make a request for each target - url := fmt.Sprintf("%s/generate?target=%s", remoteHost, target) + url := fmt.Sprintf("%s:%d/generate?target=%s", remoteHost, remotePort, target) res, body, err := util.MakeRequest(url, http.MethodGet, nil, headers) if err != nil { - log.Error().Err(err).Msg("failed to fetch files") + log.Error().Err(err).Msg("failed to make request") return } // handle getting other error codes other than a 200 if res != nil { - // NOTE: the server responses are already marshaled to JSON - fmt.Print(string(body)) + if res.StatusCode == http.StatusOK { + log.Info().Msgf("%s\n", string(body)) + } } } }, } func init() { - fetchCmd.Flags().StringVar(&remoteHost, "host", "", "set the remote configurator host and port") + fetchCmd.Flags().StringVar(&remoteHost, "host", "", "set the remote configurator host") + fetchCmd.Flags().IntVar(&remotePort, "port", 3334, "set the remote configurator port") fetchCmd.Flags().StringSliceVar(&targets, "target", nil, "set the target configs to make") fetchCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for config targets") fetchCmd.Flags().StringVar(&accessToken, "access-token", "o", "set the output path for config targets") diff --git a/cmd/generate.go b/cmd/generate.go index 578c9f0..15c5e02 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -9,19 +9,16 @@ import ( "os" "path/filepath" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "github.com/OpenCHAMI/configurator/pkg" "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) var ( tokenFetchRetries int - templatePaths []string - pluginPath string - useCompression bool + pluginPaths []string + cacertPath string ) var generateCmd = &cobra.Command{ @@ -29,76 +26,43 @@ var generateCmd = &cobra.Command{ Short: "Generate a config file from state management", Run: func(cmd *cobra.Command, args []string) { // make sure that we have a token present before trying to make request - if conf.AccessToken == "" { + if config.AccessToken == "" { + // TODO: make request to check if request will need token + // check if OCHAMI_ACCESS_TOKEN env var is set if no access token is provided and use that instead accessToken := os.Getenv("ACCESS_TOKEN") if accessToken != "" { - conf.AccessToken = accessToken + config.AccessToken = accessToken } else { // TODO: try and fetch token first if it is needed if verbose { - log.Warn().Msg("No token found. Attempting to generate conf without one...\n") + fmt.Printf("No token found. Attempting to generate config without one...\n") } } } // use cert path from cobra if empty - if conf.CertPath == "" { - conf.CertPath = cacertPath + // TODO: this needs to be checked for the correct desired behavior + if config.CertPath == "" { + config.CertPath = cacertPath } - // show conf as JSON and generators if verbose + // use config plugins if none supplied via CLI + if len(pluginPaths) <= 0 { + pluginPaths = append(pluginPaths, config.PluginDirs...) + } + + // show config as JSON and generators if verbose if verbose { - b, err := json.MarshalIndent(conf, "", " ") + b, err := json.MarshalIndent(config, "", " ") if err != nil { - log.Error().Err(err).Msg("failed to marshal config") + fmt.Printf("failed to marshal config: %v\n", err) } - // print the config file as JSON fmt.Printf("%v\n", string(b)) } - // run all of the target recursively until completion if provided - if len(targets) > 0 { - RunTargets(&conf, args, targets...) - } else { - if pluginPath == "" { - log.Error().Msg("no plugin path specified") - return - } + RunTargets(&config, args, targets...) - // load the templates to use - templates := map[string]generator.Template{} - for _, path := range templatePaths { - template := generator.Template{} - template.LoadFromFile(path) - if !template.IsEmpty() { - templates[path] = template - } - } - - params := generator.Params{ - Templates: templates, - } - - // set the client options - // params.ClientOpts = append(params.ClientOpts, client.WithHost(remoteHost)) - if conf.AccessToken != "" { - params.ClientOpts = append(params.ClientOpts, client.WithAccessToken(conf.AccessToken)) - } - if conf.CertPath != "" { - params.ClientOpts = append(params.ClientOpts, client.WithCertPoolFile(conf.CertPath)) - } - - // run generator.Generate() with just plugin path and templates provided - outputBytes, err := generator.Generate(&conf, pluginPath, params) - if err != nil { - log.Error().Err(err).Msg("failed to generate files") - } - - // if we have more than one target and output is set, create configs in directory - outputMap := generator.ConvertContentsToString(outputBytes) - writeOutput(outputBytes, len(targets), len(outputMap)) - } }, } @@ -108,105 +72,82 @@ var generateCmd = &cobra.Command{ // child targets if it is the same as the parent. // // NOTE: This may be changed in the future how this is done. -func RunTargets(conf *config.Config, args []string, targets ...string) { +func RunTargets(config *configurator.Config, args []string, targets ...string) { // generate config with each supplied target for _, target := range targets { - outputBytes, err := generator.GenerateWithTarget(conf, target) + params := generator.Params{ + Args: args, + PluginPaths: pluginPaths, + Target: target, + Verbose: verbose, + } + outputBytes, err := generator.GenerateWithTarget(config, params) if err != nil { - log.Error().Err(err).Str("target", target).Msg("failed to generate config") + fmt.Printf("failed to generate config: %v\n", err) os.Exit(1) } + outputMap := generator.ConvertContentsToString(outputBytes) + // if we have more than one target and output is set, create configs in directory - outputMap := generator.ConvertContentsToString(outputBytes) - writeOutput(outputBytes, len(targets), len(outputMap)) + var ( + targetCount = len(targets) + templateCount = len(outputMap) + ) + if outputPath == "" { + // write only to stdout by default + if len(outputMap) == 1 { + for _, contents := range outputMap { + fmt.Printf("%s\n", string(contents)) + } + } else { + for path, contents := range outputMap { + fmt.Printf("-- file: %s, size: %d B\n%s\n", path, len(contents), string(contents)) + } + } + } else if outputPath != "" && targetCount == 1 && templateCount == 1 { + // write just a single file using provided name + for _, contents := range outputBytes { + err := os.WriteFile(outputPath, contents, 0o644) + if err != nil { + fmt.Printf("failed to write config to file: %v", err) + os.Exit(1) + } + fmt.Printf("wrote file to '%s'\n", outputPath) + } + } else if outputPath != "" && targetCount > 1 || templateCount > 1 { + // write multiple files in directory using template name + err := os.MkdirAll(filepath.Clean(outputPath), 0o755) + if err != nil { + fmt.Printf("failed to make output directory: %v\n", err) + os.Exit(1) + } + for path, contents := range outputBytes { + filename := filepath.Base(path) + cleanPath := fmt.Sprintf("%s/%s", filepath.Clean(outputPath), filename) + err := os.WriteFile(cleanPath, contents, 0o755) + if err != nil { + fmt.Printf("failed to write config to file: %v\n", err) + os.Exit(1) + } + fmt.Printf("wrote file to '%s'\n", cleanPath) + } + } // remove any targets that are the same as current to prevent infinite loop - nextTargets := util.CopyIf(conf.Targets[target].RunTargets, func(nextTarget string) bool { - return nextTarget != target - }) + nextTargets := util.CopyIf(config.Targets[target].RunTargets, func(t string) bool { return t != target }) // ...then, run any other targets that the current target has - RunTargets(conf, args, nextTargets...) - } -} - -func writeOutput(outputBytes generator.FileMap, targetCount int, templateCount int) { - outputMap := generator.ConvertContentsToString(outputBytes) - if outputPath == "" { - // write only to stdout by default - if len(outputMap) == 1 { - for _, contents := range outputMap { - fmt.Printf("%s\n", string(contents)) - } - } else { - for path, contents := range outputMap { - fmt.Printf("-- file: %s, size: %d B\n%s\n", path, len(contents), string(contents)) - } - } - } else if outputPath != "" && targetCount == 1 && templateCount == 1 { - // write just a single file using provided name - for _, contents := range outputBytes { - err := os.WriteFile(outputPath, contents, 0o644) - if err != nil { - log.Error().Err(err).Str("path", outputPath).Msg("failed to write config file") - os.Exit(1) - } - log.Info().Msgf("wrote file to '%s'\n", outputPath) - } - } else if outputPath != "" && targetCount > 1 && useCompression { - // write multiple files to archive, compress, then save to output path - out, err := os.Create(fmt.Sprintf("%s.tar.gz", outputPath)) - if err != nil { - log.Error().Err(err).Str("path", outputPath).Msg("failed to write archive") - os.Exit(1) - } - files := make([]string, len(outputBytes)) - i := 0 - for path := range outputBytes { - files[i] = path - i++ - } - err = util.CreateArchive(files, out) - if err != nil { - log.Error().Err(err).Str("path", outputPath).Msg("failed to create archive") - os.Exit(1) - } - - } else if outputPath != "" && targetCount > 1 || templateCount > 1 { - // write multiple files in directory using template name - err := os.MkdirAll(filepath.Clean(outputPath), 0o755) - if err != nil { - log.Error().Err(err).Str("path", filepath.Clean(outputPath)).Msg("failed to make output directory") - os.Exit(1) - } - for path, contents := range outputBytes { - filename := filepath.Base(path) - cleanPath := fmt.Sprintf("%s/%s", filepath.Clean(outputPath), filename) - err := os.WriteFile(cleanPath, contents, 0o755) - if err != nil { - log.Error().Err(err).Str("path", path).Msg("failed to write config to file") - os.Exit(1) - } - log.Info().Msgf("wrote file to '%s'\n", cleanPath) - } + RunTargets(config, args, nextTargets...) } } func init() { - generateCmd.Flags().StringSliceVar(&targets, "target", []string{}, "set the targets to run pre-defined conf") - generateCmd.Flags().StringSliceVar(&templatePaths, "template", []string{}, "set the paths for the Jinja 2 templates to use") - generateCmd.Flags().StringVar(&pluginPath, "plugin", "", "set the generator plugin path") - generateCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for conf targets") + generateCmd.Flags().StringSliceVar(&targets, "target", []string{}, "set the target configs to make") + generateCmd.Flags().StringSliceVar(&pluginPaths, "plugins", []string{}, "set the generator plugins directory path") + generateCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for config targets") + generateCmd.Flags().StringVar(&cacertPath, "ca-cert", "", "path to CA cert. (defaults to system CAs)") generateCmd.Flags().IntVar(&tokenFetchRetries, "fetch-retries", 5, "set the number of retries to fetch an access token") - generateCmd.Flags().StringVar(&remoteHost, "host", "http://localhost", "set the remote host") - generateCmd.Flags().BoolVar(&useCompression, "compress", false, "set whether to archive and compress multiple file outputs") - - // requires either 'target' by itself or 'plugin' and 'templates' together - // generateCmd.MarkFlagsOneRequired("target", "plugin") - generateCmd.MarkFlagsMutuallyExclusive("target", "plugin") - generateCmd.MarkFlagsMutuallyExclusive("target", "template") - generateCmd.MarkFlagsRequiredTogether("plugin", "template") rootCmd.AddCommand(generateCmd) } diff --git a/cmd/inspect.go b/cmd/inspect.go index 735fa7b..2635b70 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/OpenCHAMI/configurator/pkg/generator" - "github.com/OpenCHAMI/configurator/pkg/util" "github.com/rodaine/table" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -27,16 +26,11 @@ var inspectCmd = &cobra.Command{ return strings.ToUpper(fmt.Sprintf(format, vals...)) } - // remove duplicate clean paths from CLI - paths := make([]string, len(args)) - for _, path := range args { - paths = append(paths, filepath.Clean(path)) - } - paths = util.RemoveDuplicates(paths) + // TODO: remove duplicate args from CLI // load specific plugins from positional args var generators = make(map[string]generator.Generator) - for _, path := range paths { + for _, path := range args { err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { if err != nil { return err diff --git a/cmd/root.go b/cmd/root.go index 583ee6c..cbbfab7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,26 +4,22 @@ import ( "fmt" "os" - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "github.com/OpenCHAMI/configurator/pkg" "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) var ( - conf config.Config - configPath string - cacertPath string - verbose bool - targets []string - outputPath string - accessToken string - remoteHost string + configPath string + config configurator.Config + verbose bool + targets []string + outputPath string ) var rootCmd = &cobra.Command{ Use: "configurator", - Short: "Dynamically generate files defined by generators", + Short: "Tool for building common config files", Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { cmd.Help() @@ -40,30 +36,24 @@ func Execute() { } func init() { - cobra.OnInitialize(InitConfig) - rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config path") + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "./config.yaml", "set the config path") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "set to enable verbose output") - rootCmd.PersistentFlags().StringVar(&cacertPath, "cacert", "", "path to CA cert. (defaults to system CAs)") } -func InitConfig() { - // empty from not being set +func initConfig() { if configPath != "" { exists, err := util.PathExists(configPath) if err != nil { - log.Error().Err(err).Str("path", configPath).Msg("failed to load config") + fmt.Printf("failed to load config") os.Exit(1) } else if exists { - conf = config.Load(configPath) + config = configurator.LoadConfig(configPath) } else { - // show error and exit since a path was specified - log.Error().Str("path", configPath).Msg("config file not found") - os.Exit(1) + config = configurator.NewConfig() } } else { - // set to the default value and create a new one - configPath = "./config.yaml" - conf = config.New() + config = configurator.NewConfig() } // @@ -73,6 +63,6 @@ func InitConfig() { // set the JWKS url if we find the CONFIGURATOR_JWKS_URL environment variable jwksUrl := os.Getenv("CONFIGURATOR_JWKS_URL") if jwksUrl != "" { - conf.Server.Jwks.Uri = jwksUrl + config.Server.Jwks.Uri = jwksUrl } } diff --git a/cmd/serve.go b/cmd/serve.go index 48e3629..ee54f6c 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -10,8 +10,8 @@ import ( "net/http" "os" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/server" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -20,48 +20,67 @@ var serveCmd = &cobra.Command{ Short: "Start configurator as a server and listen for requests", Run: func(cmd *cobra.Command, args []string) { // make sure that we have a token present before trying to make request - if conf.AccessToken == "" { - // check if ACCESS_TOKEN env var is set if no access token is provided and use that instead + if config.AccessToken == "" { + // TODO: make request to check if request will need token + + // check if OCHAMI_ACCESS_TOKEN env var is set if no access token is provided and use that instead accessToken := os.Getenv("ACCESS_TOKEN") if accessToken != "" { - conf.AccessToken = accessToken + config.AccessToken = accessToken } else { + // TODO: try and fetch token first if it is needed if verbose { - log.Warn().Msg("No token found. Continuing without one...\n") + fmt.Printf("No token found. Attempting to generate config without one...\n") } } } + // use config plugins if none supplied via CLI + if len(pluginPaths) <= 0 { + pluginPaths = append(pluginPaths, config.PluginDirs...) + } + // show config as JSON and generators if verbose if verbose { - b, err := json.MarshalIndent(conf, "", "\t") + b, err := json.MarshalIndent(config, "", " ") if err != nil { - log.Error().Err(err).Msg("failed to marshal config") - os.Exit(1) + fmt.Printf("failed to marshal config: %v\n", err) } fmt.Printf("%v\n", string(b)) } - // start listening with the server - var ( - s *server.Server = server.New(&conf) - err error = s.Serve() - ) + // set up the routes and start the server + server := server.Server{ + Config: &config, + Server: &http.Server{ + Addr: fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port), + }, + Jwks: server.Jwks{ + Uri: config.Server.Jwks.Uri, + Retries: config.Server.Jwks.Retries, + }, + GeneratorParams: generator.Params{ + Args: args, + PluginPaths: pluginPaths, + // Target: target, // NOTE: targets are set via HTTP requests (ex: curl http://configurator:3334/generate?target=dnsmasq) + Verbose: verbose, + }, + } + err := server.Serve() if errors.Is(err, http.ErrServerClosed) { - if verbose { - log.Info().Msg("server closed") - } + fmt.Printf("Server closed.") } else if err != nil { - log.Error().Err(err).Msg("failed to start server") + fmt.Errorf("failed to start server: %v", err) os.Exit(1) } }, } func init() { - serveCmd.Flags().StringVar(&conf.Server.Host, "host", conf.Server.Host, "set the server host and port") - // serveCmd.Flags().StringVar(&pluginPath, "plugin", "", "set the generator plugins directory path") - serveCmd.Flags().StringVar(&conf.Server.Jwks.Uri, "jwks-uri", conf.Server.Jwks.Uri, "set the JWKS url to fetch public key") - serveCmd.Flags().IntVar(&conf.Server.Jwks.Retries, "jwks-fetch-retries", conf.Server.Jwks.Retries, "set the JWKS fetch retry count") + serveCmd.Flags().StringVar(&config.Server.Host, "host", config.Server.Host, "set the server host") + serveCmd.Flags().IntVar(&config.Server.Port, "port", config.Server.Port, "set the server port") + serveCmd.Flags().StringSliceVar(&pluginPaths, "plugins", nil, "set the generator plugins directory path") + serveCmd.Flags().StringVar(&config.Server.Jwks.Uri, "jwks-uri", config.Server.Jwks.Uri, "set the JWKS url to fetch public key") + serveCmd.Flags().IntVar(&config.Server.Jwks.Retries, "jwks-fetch-retries", config.Server.Jwks.Retries, "set the JWKS fetch retry count") rootCmd.AddCommand(serveCmd) } diff --git a/examples/plugin/test.go b/examples/plugin/test.go deleted file mode 100644 index 58aa310..0000000 --- a/examples/plugin/test.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/generator" -) - -type TestGenerator struct{} - -func (g *TestGenerator) GetName() string { return "test" } -func (g *TestGenerator) GetVersion() string { return "v1.0.0" } -func (g *TestGenerator) GetDescription() string { - return "This is a plugin creating for running tests." -} -func (g *TestGenerator) Generate(config *config.Config, params generator.Params) (generator.FileMap, error) { - return generator.ApplyTemplates(generator.Mappings{ - "plugin_name": g.GetName(), - "plugin_version": g.GetVersion(), - "plugin_description": g.GetDescription(), - }, params.Templates) -} - -var Generator TestGenerator diff --git a/examples/templates/dnsmasq.jinja b/examples/templates/dnsmasq.jinja index d187a9a..c522039 100644 --- a/examples/templates/dnsmasq.jinja +++ b/examples/templates/dnsmasq.jinja @@ -7,4 +7,4 @@ # Source code: https://github.com/OpenCHAMI/configurator # Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins # -{{ dhcp_hosts }} +{{ dhcp-hosts }} diff --git a/examples/templates/test.j2 b/examples/templates/test.j2 deleted file mode 100644 index df2bb4e..0000000 --- a/examples/templates/test.j2 +++ /dev/null @@ -1,16 +0,0 @@ -# -# This file was auto-generated by the OpenCHAMI "configurator" tool using the following plugin: -# Name: {{ plugin_name }} -# Version: {{ plugin_version }} -# Description: {{ plugin_description }} -# -# Source code: https://github.com/OpenCHAMI/configurator -# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins -# - -# TODO: test variables - -# TODO: test if/else statements - -# TODO: test for loops - diff --git a/pkg/auth.go b/pkg/auth.go index c857102..58aea04 100644 --- a/pkg/auth.go +++ b/pkg/auth.go @@ -8,9 +8,26 @@ import ( "slices" "github.com/OpenCHAMI/jwtauth/v5" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/jwk" ) +func VerifyClaims(testClaims []string, r *http.Request) (bool, error) { + // extract claims from JWT + _, claims, err := jwtauth.FromContext(r.Context()) + if err != nil { + return false, fmt.Errorf("failed to get claims(s) from token: %v", err) + } + + // verify that each one of the test claims are included + for _, testClaim := range testClaims { + _, ok := claims[testClaim] + if !ok { + return false, fmt.Errorf("failed to verify claim(s) from token: %s", testClaim) + } + } + return true, nil +} + func VerifyScope(testScopes []string, r *http.Request) (bool, error) { // extract the scopes from JWT var scopes []string @@ -95,7 +112,3 @@ func FetchPublicKeyFromURL(url string) (*jwtauth.JWTAuth, error) { return tokenAuth, nil } - -func LoadAccessToken() { - -} diff --git a/pkg/client/smd.go b/pkg/client.go similarity index 52% rename from pkg/client/smd.go rename to pkg/client.go index eb80ce8..ddbfab0 100644 --- a/pkg/client/smd.go +++ b/pkg/client.go @@ -1,64 +1,128 @@ -package client +package configurator import ( "bytes" + "crypto/tls" + "crypto/x509" "encoding/json" "fmt" "io" + "net" "net/http" + "os" + "time" - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/rs/zerolog/log" + "github.com/OpenCHAMI/configurator/pkg/util" ) +type ClientOption func(*SmdClient) + // An struct that's meant to extend functionality of the base HTTP client by // adding commonly made requests to SMD. The implemented functions are can be // used in generator plugins to fetch data when it is needed to substitute // values for the Jinja templates used. type SmdClient struct { - http.Client `json:"-" yaml:"-"` + http.Client `json:"-"` Host string `yaml:"host"` Port int `yaml:"port"` AccessToken string `yaml:"access-token"` } -// Constructor function that allows supplying Option arguments to set +// Constructor function that allows supplying ClientOption arguments to set // things like the host, port, access token, etc. -func NewSmdClient(opts ...Option) SmdClient { - var ( - params = ToParams(opts...) - client = SmdClient{ - Host: params.Host, - AccessToken: params.AccessToken, - } - ) - +func NewSmdClient(opts ...ClientOption) SmdClient { + client := SmdClient{} + for _, opt := range opts { + opt(&client) + } return client } +func WithHost(host string) ClientOption { + return func(c *SmdClient) { + c.Host = host + } +} + +func WithPort(port int) ClientOption { + return func(c *SmdClient) { + c.Port = port + } +} + +func WithAccessToken(token string) ClientOption { + return func(c *SmdClient) { + c.AccessToken = token + } +} + +func WithCertPool(certPool *x509.CertPool) ClientOption { + return func(c *SmdClient) { + c.Client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + InsecureSkipVerify: true, + }, + DisableKeepAlives: true, + Dial: (&net.Dialer{ + Timeout: 120 * time.Second, + KeepAlive: 120 * time.Second, + }).Dial, + TLSHandshakeTimeout: 120 * time.Second, + ResponseHeaderTimeout: 120 * time.Second, + } + } +} + +// FIXME: Need to check for errors when reading from a file +func WithCertPoolFile(certPath string) ClientOption { + if certPath == "" { + return func(sc *SmdClient) {} + } + cacert, _ := os.ReadFile(certPath) + certPool := x509.NewCertPool() + certPool.AppendCertsFromPEM(cacert) + return WithCertPool(certPool) +} + +func WithVerbosity() util.Option { + return func(p util.Params) { + p["verbose"] = true + } +} + +// Create a set of params with all default values. +func NewParams() util.Params { + return util.Params{ + "verbose": false, + } +} + // Fetch the ethernet interfaces from SMD service using its API. An access token may be required if the SMD // service SMD_JWKS_URL envirnoment variable is set. -func (client *SmdClient) FetchEthernetInterfaces(verbose bool) ([]configurator.EthernetInterface, error) { +func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]EthernetInterface, error) { var ( - eths = []configurator.EthernetInterface{} - bytes []byte - err error + params = util.ToDict(opts...) + verbose = util.Get[bool](params, "verbose") + eths = []EthernetInterface{} ) // make request to SMD endpoint - bytes, err = client.makeRequest("/Inventory/EthernetInterfaces") + b, err := client.makeRequest("/Inventory/EthernetInterfaces") if err != nil { return nil, fmt.Errorf("failed to read HTTP response: %v", err) } // unmarshal response body JSON and extract in object - err = json.Unmarshal(bytes, ðs) + err = json.Unmarshal(b, ðs) if err != nil { return nil, fmt.Errorf("failed to unmarshal response: %v", err) } // print what we got if verbose is set - if verbose { - log.Info().Str("ethernet_interfaces", string(bytes)).Msg("found interfaces") + if verbose != nil { + if *verbose { + fmt.Printf("Ethernet Interfaces: %v\n", string(b)) + } } return eths, nil @@ -66,68 +130,68 @@ func (client *SmdClient) FetchEthernetInterfaces(verbose bool) ([]configurator.E // Fetch the components from SMD using its API. An access token may be required if the SMD // service SMD_JWKS_URL envirnoment variable is set. -func (client *SmdClient) FetchComponents(verbose bool) ([]configurator.Component, error) { +func (client *SmdClient) FetchComponents(opts ...util.Option) ([]Component, error) { var ( - comps = []configurator.Component{} - bytes []byte - err error + params = util.ToDict(opts...) + verbose = util.Get[bool](params, "verbose") + comps = []Component{} ) // make request to SMD endpoint - bytes, err = client.makeRequest("/State/Components") + b, err := client.makeRequest("/State/Components") if err != nil { return nil, fmt.Errorf("failed to make HTTP request: %v", err) } // make sure our response is actually JSON - if !json.Valid(bytes) { - return nil, fmt.Errorf("expected valid JSON response: %v", string(bytes)) + if !json.Valid(b) { + return nil, fmt.Errorf("expected valid JSON response: %v", string(b)) } // unmarshal response body JSON and extract in object var tmp map[string]any - err = json.Unmarshal(bytes, &tmp) + err = json.Unmarshal(b, &tmp) if err != nil { return nil, fmt.Errorf("failed to unmarshal response: %v", err) } - bytes, err = json.Marshal(tmp["RedfishEndpoints"].([]any)) + b, err = json.Marshal(tmp["RedfishEndpoints"].([]any)) if err != nil { return nil, fmt.Errorf("failed to marshal JSON: %v", err) } - err = json.Unmarshal(bytes, &comps) + err = json.Unmarshal(b, &comps) if err != nil { return nil, fmt.Errorf("failed to unmarshal response: %v", err) } // print what we got if verbose is set - if verbose { - log.Info().Str("components", string(bytes)).Msg("found components") + if verbose != nil { + if *verbose { + fmt.Printf("Components: %v\n", string(b)) + } } return comps, nil } -// TODO: improve implementation of this function -func (client *SmdClient) FetchRedfishEndpoints(verbose bool) ([]configurator.RedfishEndpoint, error) { +func (client *SmdClient) FetchRedfishEndpoints(opts ...util.Option) ([]RedfishEndpoint, error) { var ( - eps = []configurator.RedfishEndpoint{} - tmp map[string]any + params = util.ToDict(opts...) + verbose = util.Get[bool](params, "verbose") + eps = []RedfishEndpoint{} ) - // make initial request to get JSON with 'RedfishEndpoints' as property b, err := client.makeRequest("/Inventory/RedfishEndpoints") if err != nil { return nil, fmt.Errorf("failed to make HTTP resquest: %v", err) } - // make sure response is in JSON if !json.Valid(b) { return nil, fmt.Errorf("expected valid JSON response: %v", string(b)) } + var tmp map[string]any err = json.Unmarshal(b, &tmp) if err != nil { return nil, fmt.Errorf("failed to unmarshal response: %v", err) } - // marshal RedfishEndpoint JSON back to configurator.RedfishEndpoint b, err = json.Marshal(tmp["RedfishEndpoints"].([]any)) if err != nil { return nil, fmt.Errorf("failed to marshal JSON: %v", err) @@ -137,9 +201,10 @@ func (client *SmdClient) FetchRedfishEndpoints(verbose bool) ([]configurator.Red return nil, fmt.Errorf("failed to unmarshal response: %v", err) } - // show the final result - if verbose { - log.Info().Str("redfish_endpoints", string(b)).Msg("found redfish endpoints") + if verbose != nil { + if *verbose { + fmt.Printf("Redfish endpoints: %v\n", string(b)) + } } return eps, nil @@ -151,7 +216,7 @@ func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) { } // fetch DHCP related information from SMD's endpoint: - url := fmt.Sprintf("%s/hsm/v2%s", client.Host, endpoint) + url := fmt.Sprintf("%s:%d/hsm/v2%s", client.Host, client.Port, endpoint) req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer([]byte{})) if err != nil { return nil, fmt.Errorf("failed to create new HTTP request: %v", err) diff --git a/pkg/client/client.go b/pkg/client/client.go deleted file mode 100644 index a4b7454..0000000 --- a/pkg/client/client.go +++ /dev/null @@ -1,66 +0,0 @@ -package client - -import ( - "crypto/tls" - "crypto/x509" - "net" - "net/http" - "os" - "time" -) - -type Option func(*Params) -type Params struct { - Host string `yaml:"host"` - AccessToken string `yaml:"access-token"` - Transport *http.Transport -} - -func ToParams(opts ...Option) *Params { - params := &Params{} - for _, opt := range opts { - opt(params) - } - return params -} - -func WithHost(host string) Option { - return func(c *Params) { - c.Host = host - } -} - -func WithAccessToken(token string) Option { - return func(c *Params) { - c.AccessToken = token - } -} - -func WithCertPool(certPool *x509.CertPool) Option { - return func(c *Params) { - c.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: certPool, - InsecureSkipVerify: true, - }, - DisableKeepAlives: true, - Dial: (&net.Dialer{ - Timeout: 120 * time.Second, - KeepAlive: 120 * time.Second, - }).Dial, - TLSHandshakeTimeout: 120 * time.Second, - ResponseHeaderTimeout: 120 * time.Second, - } - } -} - -// FIXME: Need to check for errors when reading from a file -func WithCertPoolFile(certPath string) Option { - if certPath == "" { - return func(sc *Params) {} - } - cacert, _ := os.ReadFile(certPath) - certPool := x509.NewCertPool() - certPool.AppendCertsFromPEM(cacert) - return WithCertPool(certPool) -} diff --git a/pkg/config.go b/pkg/config.go new file mode 100644 index 0000000..89db7ab --- /dev/null +++ b/pkg/config.go @@ -0,0 +1,129 @@ +package configurator + +import ( + "log" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +type Options struct{} + +type Target struct { + PluginPath string `yaml:"plugin,omitempty"` + TemplatePaths []string `yaml:"templates,omitempty"` + FilePaths []string `yaml:"files,omitempty"` + RunTargets []string `yaml:"targets,omitempty"` +} + +type Jwks struct { + Uri string `yaml:"uri"` + Retries int `yaml:"retries,omitempty"` +} + +type Server struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Jwks Jwks `yaml:"jwks,omitempty"` +} + +type Config struct { + Version string `yaml:"version,omitempty"` + Server Server `yaml:"server,omitempty"` + SmdClient SmdClient `yaml:"smd,omitempty"` + AccessToken string `yaml:"access-token,omitempty"` + Targets map[string]Target `yaml:"targets,omitempty"` + PluginDirs []string `yaml:"plugins,omitempty"` + CertPath string `yaml:"cacert,omitempty"` + Options Options `yaml:"options,omitempty"` +} + +// Creates a new config with default parameters. +func NewConfig() Config { + return Config{ + Version: "", + SmdClient: SmdClient{ + Host: "http://127.0.0.1", + Port: 27779, + }, + Targets: map[string]Target{ + "dnsmasq": Target{ + PluginPath: "", + TemplatePaths: []string{}, + }, + "conman": Target{ + PluginPath: "", + TemplatePaths: []string{}, + }, + "warewulf": Target{ + PluginPath: "", + TemplatePaths: []string{ + "templates/warewulf/defaults/node.jinja", + "templates/warewulf/defaults/provision.jinja", + }, + }, + }, + + PluginDirs: []string{}, + Server: Server{ + Host: "127.0.0.1", + Port: 3334, + Jwks: Jwks{ + Uri: "", + Retries: 5, + }, + }, + Options: Options{}, + } +} + +func LoadConfig(path string) Config { + var c Config = NewConfig() + file, err := os.ReadFile(path) + if err != nil { + log.Printf("failed to read config file: %v\n", err) + return c + } + err = yaml.Unmarshal(file, &c) + if err != nil { + log.Fatalf("failed to unmarshal config: %v\n", err) + return c + } + return c +} + +func (config *Config) SaveConfig(path string) { + path = filepath.Clean(path) + if path == "" || path == "." { + path = "config.yaml" + } + data, err := yaml.Marshal(config) + if err != nil { + log.Printf("failed to marshal config: %v\n", err) + return + } + err = os.WriteFile(path, data, os.ModePerm) + if err != nil { + log.Printf("failed to write default config file: %v\n", err) + return + } +} + +func SaveDefaultConfig(path string) { + path = filepath.Clean(path) + if path == "" || path == "." { + path = "config.yaml" + } + var c = NewConfig() + data, err := yaml.Marshal(c) + if err != nil { + log.Printf("failed to marshal config: %v\n", err) + return + } + err = os.WriteFile(path, data, os.ModePerm) + if err != nil { + log.Printf("failed to write default config file: %v\n", err) + return + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go deleted file mode 100644 index c20797b..0000000 --- a/pkg/config/config.go +++ /dev/null @@ -1,100 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - - "github.com/rs/zerolog/log" - - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "gopkg.in/yaml.v2" -) - -type Jwks struct { - Uri string `yaml:"uri"` - Retries int `yaml:"retries,omitempty"` -} - -type Server struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - Jwks Jwks `yaml:"jwks,omitempty"` -} - -type Config struct { - Version string `yaml:"version,omitempty"` - Server Server `yaml:"server,omitempty"` - SmdClient client.SmdClient `yaml:"smd,omitempty"` - AccessToken string `yaml:"access-token,omitempty"` - Targets map[string]configurator.Target `yaml:"targets,omitempty"` - PluginDirs []string `yaml:"plugins,omitempty"` - CertPath string `yaml:"cacert,omitempty"` -} - -// Creates a new config with default parameters. -func New() Config { - return Config{ - Version: "", - SmdClient: client.SmdClient{Host: "http://127.0.0.1:27779"}, - Targets: map[string]configurator.Target{}, - PluginDirs: []string{}, - Server: Server{ - Host: "127.0.0.1:3334", - Jwks: Jwks{ - Uri: "", - Retries: 5, - }, - }, - } -} - -func Load(path string) Config { - var c Config = New() - file, err := os.ReadFile(path) - if err != nil { - log.Error().Err(err).Msg("failed to read config file") - return c - } - err = yaml.Unmarshal(file, &c) - if err != nil { - log.Error().Err(err).Msg("failed to unmarshal config") - return c - } - return c -} - -func (config *Config) Save(path string) { - path = filepath.Clean(path) - if path == "" || path == "." { - path = "config.yaml" - } - data, err := yaml.Marshal(config) - if err != nil { - log.Error().Err(err).Msg("failed to marshal config") - return - } - err = os.WriteFile(path, data, 0o644) - if err != nil { - log.Error().Err(err).Msg("failed to write default config file") - return - } -} - -func SaveDefault(path string) { - path = filepath.Clean(path) - if path == "" || path == "." { - path = "config.yaml" - } - var c = New() - data, err := yaml.Marshal(c) - if err != nil { - log.Error().Err(err).Msg("failed to marshal config") - return - } - err = os.WriteFile(path, data, 0o644) - if err != nil { - log.Error().Err(err).Msg("failed to write default config file") - return - } -} diff --git a/pkg/configurator.go b/pkg/configurator.go index 7a19a36..68dae99 100644 --- a/pkg/configurator.go +++ b/pkg/configurator.go @@ -2,13 +2,6 @@ package configurator import "encoding/json" -type Target struct { - Plugin string `yaml:"plugin,omitempty"` // Set the plugin or it's path - TemplatePaths []string `yaml:"templates,omitempty"` // Set the template paths - FilePaths []string `yaml:"files,omitempty"` // Set the file paths - RunTargets []string `yaml:"targets,omitempty"` // Set additional targets to run -} - type IPAddr struct { IpAddress string `json:"IPAddress"` Network string `json:"Network"` diff --git a/pkg/generator/dhcpd.go b/pkg/generator/dhcpd.go deleted file mode 100644 index cc32a48..0000000 --- a/pkg/generator/dhcpd.go +++ /dev/null @@ -1,64 +0,0 @@ -package generator - -import ( - "fmt" - - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" -) - -type DHCPd struct{} - -func (g *DHCPd) GetName() string { - return "dhcpd" -} - -func (g *DHCPd) GetVersion() string { - return util.GitCommit() -} - -func (g *DHCPd) GetDescription() string { - return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) -} - -func (g *DHCPd) Generate(config *config.Config, params Params) (FileMap, error) { - var ( - smdClient = client.NewSmdClient(params.ClientOpts...) - eths = []configurator.EthernetInterface{} - computeNodes = "" - err error = nil - ) - - // - eths, err = smdClient.FetchEthernetInterfaces(params.Verbose) - if err != nil { - return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %w", err) - } - - // check if we have the required params first - if eths == nil { - return nil, fmt.Errorf("invalid ethernet interfaces (variable is nil)") - } - if len(eths) <= 0 { - return nil, fmt.Errorf("no ethernet interfaces found") - } - - // format output to write to config file - computeNodes = "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n" - for _, eth := range eths { - if len(eth.IpAddresses) == 0 { - continue - } - computeNodes += fmt.Sprintf("host %s { hardware ethernet %s; fixed-address %s} ", eth.ComponentId, eth.MacAddress, eth.IpAddresses[0]) - } - computeNodes += "# =====================================================================" - return ApplyTemplates(Mappings{ - "plugin_name": g.GetName(), - "plugin_version": g.GetVersion(), - "plugin_description": g.GetDescription(), - "compute_nodes": computeNodes, - "node_entries": "", - }, params.Templates) -} diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 3397e36..27a032e 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -1,47 +1,39 @@ package generator import ( + "bytes" "fmt" - "io/fs" + "maps" "os" "path/filepath" "plugin" configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/rs/zerolog/log" + "github.com/nikolalohinski/gonja/v2" + "github.com/nikolalohinski/gonja/v2/exec" ) -type ( - Mappings map[string]any - FileMap map[string][]byte - FileList [][]byte +type Mappings map[string]any +type FileMap map[string][]byte +type FileList [][]byte - // Generator interface used to define how files are created. Plugins can - // be created entirely independent of the main driver program. - Generator interface { - GetName() string - GetVersion() string - GetDescription() string - Generate(config *config.Config, params Params) (FileMap, error) - } -) +// Generator interface used to define how files are created. Plugins can +// be created entirely independent of the main driver program. +type Generator interface { + GetName() string + GetVersion() string + GetDescription() string + Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) +} -var DefaultGenerators = createDefaultGenerators() - -func createDefaultGenerators() map[string]Generator { - var ( - generatorMap = map[string]Generator{} - generators = []Generator{ - &Conman{}, &DHCPd{}, &DNSMasq{}, &Warewulf{}, &Example{}, &CoreDhcp{}, - } - ) - for _, g := range generators { - generatorMap[g.GetName()] = g - } - return generatorMap +// Params defined and used by the "generate" subcommand. +type Params struct { + Args []string + PluginPaths []string + Generators map[string]Generator + Target string + Verbose bool } // Converts the file outputs from map[string][]byte to map[string]string. @@ -59,13 +51,13 @@ func LoadFiles(paths ...string) (FileMap, error) { for _, path := range paths { expandedPaths, err := filepath.Glob(path) if err != nil { - return nil, fmt.Errorf("failed to glob path: %w", err) + return nil, fmt.Errorf("failed to glob path: %v", err) } for _, expandedPath := range expandedPaths { info, err := os.Stat(expandedPath) if err != nil { fmt.Println(err) - return nil, fmt.Errorf("failed to stat file or directory: %w", err) + return nil, fmt.Errorf("failed to stat file or directory: %v", err) } // skip any directories found if info.IsDir() { @@ -73,7 +65,7 @@ func LoadFiles(paths ...string) (FileMap, error) { } b, err := os.ReadFile(expandedPath) if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) + return nil, fmt.Errorf("failed to read file: %v", err) } outputs[expandedPath] = b @@ -89,19 +81,19 @@ func LoadPlugin(path string) (Generator, error) { if isDir, err := util.IsDirectory(path); err == nil && isDir { return nil, nil } else if err != nil { - return nil, fmt.Errorf("failed to test if plugin path is directory: %w", err) + return nil, fmt.Errorf("failed to test if path is directory: %v", err) } // try and open the plugin p, err := plugin.Open(path) if err != nil { - return nil, fmt.Errorf("failed to open plugin: %w", err) + return nil, fmt.Errorf("failed to open plugin: %v", err) } // load the "Generator" symbol from plugin symbol, err := p.Lookup("Generator") if err != nil { - return nil, fmt.Errorf("failed to look up symbol at path '%s': %w", path, err) + return nil, fmt.Errorf("failed to look up symbol at path '%s': %v", path, err) } // assert that the plugin loaded has a valid generator @@ -116,73 +108,153 @@ func LoadPlugin(path string) (Generator, error) { // // Returns a map of generators. Each generator can be accessed by the name // returned by the generator.GetName() implemented. -func LoadPlugins(dirpath string, opts ...Option) (map[string]Generator, error) { +func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, error) { // check if verbose option is supplied var ( - generators = make(map[string]Generator) - params = ToParams(opts...) + gens = make(map[string]Generator) + params = util.GetParams(opts...) ) - // - err := filepath.Walk(dirpath, func(path string, info fs.FileInfo, err error) error { - // skip trying to load generator plugin if directory or error - if info.IsDir() || err != nil { - return nil + items, _ := os.ReadDir(dirpath) + for _, item := range items { + if item.IsDir() { + subitems, _ := os.ReadDir(item.Name()) + for _, subitem := range subitems { + if !subitem.IsDir() { + gen, err := LoadPlugin(subitem.Name()) + if err != nil { + fmt.Printf("failed to load generator in directory '%s': %v\n", item.Name(), err) + continue + } + if verbose, ok := params["verbose"].(bool); ok { + if verbose { + fmt.Printf("-- found plugin '%s'\n", item.Name()) + } + } + gens[gen.GetName()] = gen + } + } + } else { + gen, err := LoadPlugin(dirpath + item.Name()) + if err != nil { + fmt.Printf("failed to load plugin: %v\n", err) + continue + } + if verbose, ok := params["verbose"].(bool); ok { + if verbose { + fmt.Printf("-- found plugin '%s'\n", dirpath+item.Name()) + } + } + gens[gen.GetName()] = gen } - - // only try loading if file has .so extension - if filepath.Ext(path) != ".so" { - return nil - } - - // load the generator plugin from current path - gen, err := LoadPlugin(path) - if err != nil { - return fmt.Errorf("failed to load generator in directory '%s': %w", path, err) - } - - // show the plugins found if verbose flag is set - if params.Verbose { - log.Info().Str("plugin_name", gen.GetName()).Msg("found plugin") - } - - // map each generator plugin by name for lookup - generators[gen.GetName()] = gen - return nil - }) - - if err != nil { - return nil, fmt.Errorf("failed to walk directory: %w", err) } - return generators, nil + return gens, nil } -// Generate() is the main function to generate a collection of files and returns them as a map. -// This function only expects a path to a plugin and paths to a collection of templates to -// be used. This function will only load the plugin on-demand and fetch resources as needed. -// -// This function requires that a target and plugin path be set at minimum. -func Generate(config *config.Config, plugin string, params Params) (FileMap, error) { - var ( - gen Generator - ok bool - err error - ) - - // check if generator is built-in first before loading external plugin - log.Debug().Any("generators", DefaultGenerators).Msg("available generators") - gen, ok = DefaultGenerators[plugin] - if !ok { - // only load the plugin needed for this target if we don't find default - log.Error().Str("plugin", plugin).Msg("could not find target in default generators") - gen, err = LoadPlugin(plugin) - if err != nil { - return nil, fmt.Errorf("failed to load plugin from file: %v", err) +// Option to specify "target" in parameter map. This is used to set which generator +// to use to generate a config file. +func WithTarget(target string) util.Option { + return func(p util.Params) { + if p != nil { + p["target"] = target } } +} - return gen.Generate(config, params) +// Option to specify "type" in parameter map. This is not currently used. +func WithType(_type string) util.Option { + return func(p util.Params) { + if p != nil { + p["type"] = _type + } + } +} + +// Option to a specific client to include in implementing plugin generator.Generate(). +// +// NOTE: This may be changed to pass some kind of client interface as an argument in +// the future instead. +func WithClient(client configurator.SmdClient) util.Option { + return func(p util.Params) { + p["client"] = client + } +} + +// Helper function to get client in generator.Generate() plugin implementations. +func GetClient(params util.Params) *configurator.SmdClient { + return util.Get[configurator.SmdClient](params, "client") +} + +// Helper function to get the target in generator.Generate() plugin implementations. +func GetTarget(config *configurator.Config, key string) configurator.Target { + return config.Targets[key] +} + +// Helper function to load all options set with With*() into parameter map. +func GetParams(opts ...util.Option) util.Params { + params := util.Params{} + for _, opt := range opts { + opt(params) + } + return params +} + +// Wrapper function to slightly abstract away some of the nuances with using gonja +// into a single function call. This function is *mostly* for convenience and +// simplication. If no paths are supplied, then no templates will be applied and +// there will be no output. +// +// The "FileList" returns a slice of byte arrays in the same order as the argument +// list supplied, but with the Jinja templating applied. +func ApplyTemplates(mappings Mappings, contents ...[]byte) (FileList, error) { + var ( + data = exec.NewContext(mappings) + outputs = FileList{} + ) + + for _, b := range contents { + // load jinja template from file + t, err := gonja.FromBytes(b) + if err != nil { + return nil, fmt.Errorf("failed to read template from file: %v", err) + } + + // execute/render jinja template + b := bytes.Buffer{} + if err = t.Execute(&b, data); err != nil { + return nil, fmt.Errorf("failed to execute: %v", err) + } + outputs = append(outputs, b.Bytes()) + } + + return outputs, nil +} + +// Wrapper function similiar to "ApplyTemplates" but takes file paths as arguments. +// This function will load templates from a file instead of using file contents. +func ApplyTemplateFromFiles(mappings Mappings, paths ...string) (FileMap, error) { + var ( + data = exec.NewContext(mappings) + outputs = FileMap{} + ) + + for _, path := range paths { + // load jinja template from file + t, err := gonja.FromFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read template from file: %v", err) + } + + // execute/render jinja template + b := bytes.Buffer{} + if err = t.Execute(&b, data); err != nil { + return nil, fmt.Errorf("failed to execute: %v", err) + } + outputs[path] = b.Bytes() + } + + return outputs, nil } // Main function to generate a collection of files as a map with the path as the key and @@ -194,71 +266,53 @@ func Generate(config *config.Config, plugin string, params Params) (FileMap, err // It is also call when running the configurator as a service with the "/generate" route. // // TODO: Separate loading plugins so we can load them once when running as a service. -func GenerateWithTarget(config *config.Config, target string) (FileMap, error) { +func GenerateWithTarget(config *configurator.Config, params Params) (FileMap, error) { // load generator plugins to generate configs or to print var ( - opts []client.Option - targetInfo configurator.Target - generator Generator - params Params - err error - ok bool + generators = make(map[string]Generator) + client = configurator.NewSmdClient( + configurator.WithHost(config.SmdClient.Host), + configurator.WithPort(config.SmdClient.Port), + configurator.WithAccessToken(config.AccessToken), + configurator.WithCertPoolFile(config.CertPath), + ) ) - // check if a target is supplied - if target == "" { - return nil, fmt.Errorf("must specify a target") - } - - // load target information from config - targetInfo, ok = config.Targets[target] - if !ok { - log.Warn().Str("target", target).Msg("target not found in config") - } - - // if no plugin supplied in config target, then using the target supplied - if targetInfo.Plugin == "" { - targetInfo.Plugin = target - } - - // check if generator is built-in first before loading - generator, ok = DefaultGenerators[target] - if !ok { - // only load the plugin needed for this target if we don't find default - log.Warn().Str("target", target).Msg("could not find target in default generators") - generator, err = LoadPlugin(targetInfo.Plugin) - if err != nil { - return nil, fmt.Errorf("failed to load plugin: %v", err) + // load all plugins from supplied arguments + for _, path := range params.PluginPaths { + if params.Verbose { + fmt.Printf("loading plugins from '%s'\n", path) } + plugins, err := LoadPlugins(path) + if err != nil { + fmt.Printf("failed to load plugins: %v\n", err) + err = nil + continue + } + + // add loaded generator plugins to set + maps.Copy(generators, plugins) } - // check if there's at least one template available - if len(targetInfo.TemplatePaths) <= 0 { - return nil, fmt.Errorf("expects at least one template to be available") - } + // copy all generators supplied from arguments + maps.Copy(generators, params.Generators) - // prepare params to pass into generator - params.Templates = map[string]Template{} - for _, templatePath := range targetInfo.TemplatePaths { - template := Template{} - template.LoadFromFile(templatePath) - params.Templates[templatePath] = template - } - - // set the client options - if config.AccessToken != "" { - params.ClientOpts = append(opts, client.WithAccessToken(config.AccessToken)) - } - if config.CertPath != "" { - params.ClientOpts = append(opts, client.WithCertPoolFile(config.CertPath)) - } - - // load files that are not to be copied - params.Files, err = LoadFiles(targetInfo.FilePaths...) - if err != nil { - return nil, fmt.Errorf("failed to load files to copy: %v", err) + // show available targets then exit + if len(params.Args) == 0 && params.Target == "" { + for g := range generators { + fmt.Printf("-- found generator plugin \"%s\"\n", g) + } + return nil, nil } // run the generator plugin from target passed - return generator.Generate(config, params) + gen := generators[params.Target] + if gen == nil { + return nil, fmt.Errorf("invalid generator target (%s)", params.Target) + } + return gen.Generate( + config, + WithTarget(gen.GetName()), + WithClient(client), + ) } diff --git a/pkg/generator/params.go b/pkg/generator/params.go deleted file mode 100644 index e54420a..0000000 --- a/pkg/generator/params.go +++ /dev/null @@ -1,43 +0,0 @@ -package generator - -import ( - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" -) - -type ( - // Params used by the generator - Params struct { - Templates map[string]Template - Files map[string][]byte - ClientOpts []client.Option - Verbose bool - } - Option func(Params) -) - -func ToParams(opts ...Option) Params { - params := Params{} - for _, opt := range opts { - opt(params) - } - return params -} - -func WithClientOpts(opts ...client.Option) Option { - return func(p Params) { - p.ClientOpts = opts - } -} - -func WithTemplates(templates map[string]Template) Option { - return func(p Params) { - p.Templates = templates - } -} - -// Helper function to get the target in generator.Generate() plugin implementations. -func GetTarget(config *config.Config, key string) configurator.Target { - return config.Targets[key] -} diff --git a/pkg/generator/conman.go b/pkg/generator/plugins/conman/conman.go similarity index 51% rename from pkg/generator/conman.go rename to pkg/generator/plugins/conman/conman.go index ccadaa6..6a14f89 100644 --- a/pkg/generator/conman.go +++ b/pkg/generator/plugins/conman/conman.go @@ -1,11 +1,10 @@ -package generator +package main import ( "fmt" configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -23,20 +22,32 @@ func (g *Conman) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Conman) Generate(config *config.Config, params Params) (FileMap, error) { +func (g *Conman) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { var ( - smdClient = client.NewSmdClient(params.ClientOpts...) - eps = []configurator.RedfishEndpoint{} - err error = nil - consoles = "" + params = generator.GetParams(opts...) + client = generator.GetClient(params) + targetKey = params["targets"].(string) // required param + target = config.Targets[targetKey] + eps []configurator.RedfishEndpoint = nil + err error = nil + // serverOpts = "" + // globalOpts = "" + consoles = "" ) // fetch required data from SMD to create config - eps, err = smdClient.FetchRedfishEndpoints(params.Verbose) - if err != nil { - return nil, fmt.Errorf("failed to fetch redfish endpoints with client: %v", err) + if client != nil { + eps, err = client.FetchRedfishEndpoints(opts...) + if err != nil { + return nil, fmt.Errorf("failed to fetch redfish endpoints with client: %v", err) + } } + // add any additional conman or server opts + // if extraOpts, ok := params["opts"].(map[string]any); ok { + + // } + // format output to write to config file consoles = "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n" for _, ep := range eps { @@ -45,12 +56,13 @@ func (g *Conman) Generate(config *config.Config, params Params) (FileMap, error) consoles += "# =====================================================================" // apply template substitutions and return output as byte array - return ApplyTemplates(Mappings{ + return generator.ApplyTemplateFromFiles(generator.Mappings{ "plugin_name": g.GetName(), "plugin_version": g.GetVersion(), "plugin_description": g.GetDescription(), "server_opts": "", "global_opts": "", - "consoles": consoles, - }, params.Templates) + }, target.TemplatePaths...) } + +var Generator Conman diff --git a/pkg/generator/coredhcp.go b/pkg/generator/plugins/coredhcp/coredhcp.go similarity index 64% rename from pkg/generator/coredhcp.go rename to pkg/generator/plugins/coredhcp/coredhcp.go index 0a256e9..4e0729a 100644 --- a/pkg/generator/coredhcp.go +++ b/pkg/generator/plugins/coredhcp/coredhcp.go @@ -1,9 +1,10 @@ -package generator +package main import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -21,6 +22,8 @@ func (g *CoreDhcp) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s' to generate config files. (WIP)", g.GetName()) } -func (g *CoreDhcp) Generate(config *config.Config, params Params) (FileMap, error) { +func (g *CoreDhcp) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { return nil, fmt.Errorf("plugin does not implement generation function") } + +var Generator CoreDhcp diff --git a/pkg/generator/plugins/dhcpd/dhcpd.go b/pkg/generator/plugins/dhcpd/dhcpd.go new file mode 100644 index 0000000..a736064 --- /dev/null +++ b/pkg/generator/plugins/dhcpd/dhcpd.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/util" +) + +type Dhcpd struct{} + +func (g *Dhcpd) GetName() string { + return "dhcpd" +} + +func (g *Dhcpd) GetVersion() string { + return util.GitCommit() +} + +func (g *Dhcpd) GetDescription() string { + return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) +} + +func (g *Dhcpd) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { + var ( + params = generator.GetParams(opts...) + client = generator.GetClient(params) + targetKey = params["target"].(string) + target = config.Targets[targetKey] + compute_nodes = "" + eths []configurator.EthernetInterface = nil + err error = nil + ) + + // + if client != nil { + eths, err = client.FetchEthernetInterfaces(opts...) + if err != nil { + return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) + } + } + + // check if we have the required params first + if eths == nil { + return nil, fmt.Errorf("invalid ethernet interfaces (variable is nil)") + } + if len(eths) <= 0 { + return nil, fmt.Errorf("no ethernet interfaces found") + } + + // format output to write to config file + compute_nodes = "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n" + for _, eth := range eths { + if len(eth.IpAddresses) == 0 { + continue + } + compute_nodes += fmt.Sprintf("host %s { hardware ethernet %s; fixed-address %s} ", eth.ComponentId, eth.MacAddress, eth.IpAddresses[0]) + } + compute_nodes += "# =====================================================================" + + if verbose, ok := params["verbose"].(bool); ok { + if verbose { + fmt.Printf("") + } + } + return generator.ApplyTemplateFromFiles(generator.Mappings{ + "plugin_name": g.GetName(), + "plugin_version": g.GetVersion(), + "plugin_description": g.GetDescription(), + "compute_nodes": compute_nodes, + "node_entries": "", + }, target.TemplatePaths...) +} + +var Generator Dhcpd diff --git a/pkg/generator/dnsmasq.go b/pkg/generator/plugins/dnsmasq/dnsmasq.go similarity index 52% rename from pkg/generator/dnsmasq.go rename to pkg/generator/plugins/dnsmasq/dnsmasq.go index 8db68ba..9150009 100644 --- a/pkg/generator/dnsmasq.go +++ b/pkg/generator/plugins/dnsmasq/dnsmasq.go @@ -1,29 +1,29 @@ -package generator +package main import ( "fmt" + "strings" configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/util" ) -type DNSMasq struct{} +type DnsMasq struct{} -func (g *DNSMasq) GetName() string { +func (g *DnsMasq) GetName() string { return "dnsmasq" } -func (g *DNSMasq) GetVersion() string { +func (g *DnsMasq) GetVersion() string { return util.GitCommit() } -func (g *DNSMasq) GetDescription() string { +func (g *DnsMasq) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *DNSMasq) Generate(config *config.Config, params Params) (FileMap, error) { +func (g *DnsMasq) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { // make sure we have a valid config first if config == nil { return nil, fmt.Errorf("invalid config (config is nil)") @@ -31,15 +31,20 @@ func (g *DNSMasq) Generate(config *config.Config, params Params) (FileMap, error // set all the defaults for variables var ( - smdClient = client.NewSmdClient(params.ClientOpts...) - eths = []configurator.EthernetInterface{} - err error = nil + params = generator.GetParams(opts...) + client = generator.GetClient(params) + targetKey = params["target"].(string) // required param + target = config.Targets[targetKey] + eths []configurator.EthernetInterface = nil + err error = nil ) // if we have a client, try making the request for the ethernet interfaces - eths, err = smdClient.FetchEthernetInterfaces(params.Verbose) - if err != nil { - return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) + if client != nil { + eths, err = client.FetchEthernetInterfaces(opts...) + if err != nil { + return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) + } } // check if we have the required params first @@ -50,6 +55,13 @@ func (g *DNSMasq) Generate(config *config.Config, params Params) (FileMap, error return nil, fmt.Errorf("no ethernet interfaces found") } + // print message if verbose param found + if verbose, ok := params["verbose"].(bool); ok { + if verbose { + fmt.Printf("template: \n%s\nethernet interfaces found: %v\n", strings.Join(target.TemplatePaths, "\n\t"), len(eths)) + } + } + // format output to write to config file output := "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n" for _, eth := range eths { @@ -62,10 +74,12 @@ func (g *DNSMasq) Generate(config *config.Config, params Params) (FileMap, error output += "# =====================================================================" // apply template substitutions and return output as byte array - return ApplyTemplates(Mappings{ + return generator.ApplyTemplateFromFiles(generator.Mappings{ "plugin_name": g.GetName(), "plugin_version": g.GetVersion(), "plugin_description": g.GetDescription(), - "dhcp_hosts": output, - }, params.Templates) + "dhcp-hosts": output, + }, target.TemplatePaths...) } + +var Generator DnsMasq diff --git a/pkg/generator/example.go b/pkg/generator/plugins/example/example.go similarity index 63% rename from pkg/generator/example.go rename to pkg/generator/plugins/example/example.go index f18abe4..64cd3bc 100644 --- a/pkg/generator/example.go +++ b/pkg/generator/plugins/example/example.go @@ -1,9 +1,10 @@ -package generator +package main import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -23,9 +24,11 @@ func (g *Example) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Example) Generate(config *config.Config, params Params) (FileMap, error) { +func (g *Example) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { g.Message = ` This is an example generator plugin. See the file in 'internal/generator/plugins/example/example.go' on information about constructing plugins and plugin requirements.` - return FileMap{"example": []byte(g.Message)}, nil + return generator.FileMap{"example": []byte(g.Message)}, nil } + +var Generator Example diff --git a/pkg/generator/hostfile.go b/pkg/generator/plugins/hostfile/hostfile.go similarity index 62% rename from pkg/generator/hostfile.go rename to pkg/generator/plugins/hostfile/hostfile.go index 7ce26c8..4c611f4 100644 --- a/pkg/generator/hostfile.go +++ b/pkg/generator/plugins/hostfile/hostfile.go @@ -1,9 +1,10 @@ -package generator +package main import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -21,6 +22,8 @@ func (g *Hostfile) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Hostfile) Generate(config *config.Config, opts ...Option) (FileMap, error) { +func (g *Hostfile) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { return nil, fmt.Errorf("plugin does not implement generation function") } + +var Generator Hostfile diff --git a/pkg/generator/powerman.go b/pkg/generator/plugins/powerman/powerman.go similarity index 62% rename from pkg/generator/powerman.go rename to pkg/generator/plugins/powerman/powerman.go index 08745e5..1dca29e 100644 --- a/pkg/generator/powerman.go +++ b/pkg/generator/plugins/powerman/powerman.go @@ -1,9 +1,10 @@ -package generator +package main import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -21,6 +22,8 @@ func (g *Powerman) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Powerman) Generate(config *config.Config, opts ...Option) (FileMap, error) { +func (g *Powerman) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { return nil, fmt.Errorf("plugin does not implement generation function") } + +var Generator Powerman diff --git a/pkg/generator/syslog.go b/pkg/generator/plugins/syslog/syslog.go similarity index 62% rename from pkg/generator/syslog.go rename to pkg/generator/plugins/syslog/syslog.go index 67b28cf..94ea295 100644 --- a/pkg/generator/syslog.go +++ b/pkg/generator/plugins/syslog/syslog.go @@ -1,9 +1,10 @@ -package generator +package main import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -21,6 +22,8 @@ func (g *Syslog) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Syslog) Generate(config *config.Config, opts ...Option) (FileMap, error) { +func (g *Syslog) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { return nil, fmt.Errorf("plugin does not implement generation function") } + +var Generator Syslog diff --git a/pkg/generator/plugins/warewulf/warewulf.go b/pkg/generator/plugins/warewulf/warewulf.go new file mode 100644 index 0000000..8f40d7a --- /dev/null +++ b/pkg/generator/plugins/warewulf/warewulf.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "maps" + "strings" + + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/util" +) + +type Warewulf struct{} + +func (g *Warewulf) GetName() string { + return "warewulf" +} + +func (g *Warewulf) GetVersion() string { + return util.GitCommit() +} + +func (g *Warewulf) GetDescription() string { + return "Configurator generator plugin for 'warewulf' config files." +} + +func (g *Warewulf) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { + var ( + params = generator.GetParams(opts...) + client = generator.GetClient(params) + targetKey = params["target"].(string) + target = config.Targets[targetKey] + outputs = make(generator.FileMap, len(target.FilePaths)+len(target.TemplatePaths)) + ) + + // check if our client is included and is valid + if client == nil { + return nil, fmt.Errorf("invalid client (client is nil)") + } + + // if we have a client, try making the request for the ethernet interfaces + eths, err := client.FetchEthernetInterfaces(opts...) + if err != nil { + return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) + } + + // check if we have the required params first + if eths == nil { + return nil, fmt.Errorf("invalid ethernet interfaces (variable is nil)") + } + if len(eths) <= 0 { + return nil, fmt.Errorf("no ethernet interfaces found") + } + + // print message if verbose param found + if verbose, ok := params["verbose"].(bool); ok { + if verbose { + fmt.Printf("template: \n%s\n ethernet interfaces found: %v\n", strings.Join(target.TemplatePaths, "\n\t"), len(eths)) + } + } + + // fetch redfish endpoints and handle errors + eps, err := client.FetchRedfishEndpoints(opts...) + if err != nil { + return nil, fmt.Errorf("failed to fetch redfish endpoints: %v", err) + } + if len(eps) <= 0 { + return nil, fmt.Errorf("no redfish endpoints found") + } + + // format output for template substitution + nodeEntries := "" + + // load files and templates and copy to outputs + files, err := generator.LoadFiles(target.FilePaths...) + if err != nil { + return nil, fmt.Errorf("failed to load files: %v", err) + } + templates, err := generator.ApplyTemplateFromFiles(generator.Mappings{ + "node_entries": nodeEntries, + }, target.TemplatePaths...) + if err != nil { + return nil, fmt.Errorf("failed to load templates: %v", err) + } + + maps.Copy(outputs, files) + maps.Copy(outputs, templates) + + // print message if verbose param is found + if verbose, ok := params["verbose"].(bool); ok { + if verbose { + fmt.Printf("templates and files loaded: \n") + for path, _ := range outputs { + fmt.Printf("\t%s", path) + } + } + } + + return outputs, err +} + +var Generator Warewulf diff --git a/pkg/generator/templates.go b/pkg/generator/templates.go deleted file mode 100644 index 321076c..0000000 --- a/pkg/generator/templates.go +++ /dev/null @@ -1,98 +0,0 @@ -package generator - -import ( - "bytes" - "fmt" - "os" - - "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/nikolalohinski/gonja/v2" - "github.com/nikolalohinski/gonja/v2/exec" - "github.com/rs/zerolog/log" -) - -type Template struct { - Contents []byte `json:"contents"` -} - -func (t *Template) LoadFromFile(path string) error { - // skip loading template if path is a directory with no error - if isDir, err := util.IsDirectory(path); err == nil && isDir { - return nil - } else if err != nil { - return fmt.Errorf("failed to test if template path is directory: %w", err) - } - - // try and read the contents of the file - // NOTE: we don't care if this is actually a Jinja template - // or not...at least for now. - contents, err := os.ReadFile(path) - if err != nil { - return fmt.Errorf("failed to read file: %v", err) - } - t.Contents = contents - return nil -} - -func (t *Template) IsEmpty() bool { - return len(t.Contents) <= 0 -} - -// Wrapper function to slightly abstract away some of the nuances with using gonja -// into a single function call. This function is *mostly* for convenience and -// simplication. If no paths are supplied, then no templates will be applied and -// there will be no output. -// -// The "FileList" returns a slice of byte arrays in the same order as the argument -// list supplied, but with the Jinja templating applied. -func ApplyTemplates(mappings Mappings, templates map[string]Template) (FileMap, error) { - var ( - data = exec.NewContext(mappings) - outputs = FileMap{} - ) - - for path, template := range templates { - // load jinja template from file - t, err := gonja.FromBytes(template.Contents) - if err != nil { - return nil, fmt.Errorf("failed to read template from file: %w", err) - } - - // execute/render jinja template - b := bytes.Buffer{} - if err = t.Execute(&b, data); err != nil { - return nil, fmt.Errorf("failed to execute: %w", err) - } - outputs[path] = b.Bytes() - } - - log.Debug().Any("templates", templates).Any("outputs", outputs).Any("mappings", mappings).Msg("apply templates") - - return outputs, nil -} - -// Wrapper function similiar to "ApplyTemplates" but takes file paths as arguments. -// This function will load templates from a file instead of using file contents. -func ApplyTemplateFromFiles(mappings Mappings, paths ...string) (FileMap, error) { - var ( - data = exec.NewContext(mappings) - outputs = FileMap{} - ) - - for _, path := range paths { - // load jinja template from file - t, err := gonja.FromFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read template from file: %w", err) - } - - // execute/render jinja template - b := bytes.Buffer{} - if err = t.Execute(&b, data); err != nil { - return nil, fmt.Errorf("failed to execute: %w", err) - } - outputs[path] = b.Bytes() - } - - return outputs, nil -} diff --git a/pkg/generator/warewulf.go b/pkg/generator/warewulf.go deleted file mode 100644 index bdfbda1..0000000 --- a/pkg/generator/warewulf.go +++ /dev/null @@ -1,78 +0,0 @@ -package generator - -import ( - "fmt" - "maps" - "strings" - - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/rs/zerolog/log" -) - -type Warewulf struct{} - -func (g *Warewulf) GetName() string { - return "warewulf" -} - -func (g *Warewulf) GetVersion() string { - return util.GitCommit() -} - -func (g *Warewulf) GetDescription() string { - return "Configurator generator plugin for 'warewulf' config files." -} - -func (g *Warewulf) Generate(config *config.Config, params Params) (FileMap, error) { - var ( - smdClient = client.NewSmdClient(params.ClientOpts...) - outputs = make(FileMap, len(params.Templates)) - nodeEntries = "" - paths = []string{} - ) - - // if we have a client, try making the request for the ethernet interfaces - eths, err := smdClient.FetchEthernetInterfaces(params.Verbose) - if err != nil { - return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) - } - - // check if we have the required params first - if eths == nil { - return nil, fmt.Errorf("invalid ethernet interfaces (variable is nil)") - } - if len(eths) <= 0 { - return nil, fmt.Errorf("no ethernet interfaces found") - } - - // fetch redfish endpoints and handle errors - eps, err := smdClient.FetchRedfishEndpoints(params.Verbose) - if err != nil { - return nil, fmt.Errorf("failed to fetch redfish endpoints: %v", err) - } - if len(eps) <= 0 { - return nil, fmt.Errorf("no redfish endpoints found") - } - - templates, err := ApplyTemplates(Mappings{ - "node_entries": nodeEntries, - }, params.Templates) - if err != nil { - return nil, fmt.Errorf("failed to load templates: %v", err) - } - - maps.Copy(outputs, params.Files) - maps.Copy(outputs, templates) - - // print message if verbose param is found - if params.Verbose { - for path, _ := range outputs { - paths = append(paths, path) - } - } - log.Info().Str("paths", strings.Join(paths, ":")).Msg("templates and files loaded: \n") - - return outputs, err -} diff --git a/pkg/server/server.go b/pkg/server/server.go index d10ccd7..89e0d71 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -6,19 +6,17 @@ package server import ( "encoding/json" "fmt" - "io" "net/http" "os" "time" configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/jwtauth/v5" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/rs/zerolog" + "github.com/sirupsen/logrus" openchami_authenticator "github.com/openchami/chi-middleware/auth" openchami_logger "github.com/openchami/chi-middleware/log" @@ -35,48 +33,42 @@ type Jwks struct { } type Server struct { *http.Server - Config *config.Config + Config *configurator.Config Jwks Jwks `yaml:"jwks"` GeneratorParams generator.Params TokenAuth *jwtauth.JWTAuth - Targets map[string]Target -} - -type Target struct { - Name string `json:"name"` - PluginPath string `json:"plugin"` - Templates []generator.Template `json:"templates"` } // Constructor to make a new server instance with an optional config. -func New(conf *config.Config) *Server { +func New(config *configurator.Config) *Server { // create default config if none supplied - if conf == nil { - c := config.New() - conf = &c + if config == nil { + c := configurator.NewConfig() + config = &c } - newServer := &Server{ - Config: conf, - Server: &http.Server{Addr: conf.Server.Host}, + // return based on config values + return &Server{ + Config: config, + Server: &http.Server{ + Addr: fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port), + }, Jwks: Jwks{ - Uri: conf.Server.Jwks.Uri, - Retries: conf.Server.Jwks.Retries, + Uri: config.Server.Jwks.Uri, + Retries: config.Server.Jwks.Retries, }, } - // load templates for server from config - newServer.loadTargets() - log.Debug().Any("targets", newServer.Targets).Msg("new server targets") - return newServer } // Main function to start up configurator as a service. func (s *Server) Serve() error { - // Setup logger - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix - logger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // create client just for the server to use to fetch data from SMD + _ = &configurator.SmdClient{ + Host: s.Config.SmdClient.Host, + Port: s.Config.SmdClient.Port, + } // set the server address with config values - s.Server.Addr = s.Config.Server.Host + s.Server.Addr = fmt.Sprintf("%s:%d", s.Config.Server.Host, s.Config.Server.Port) // fetch JWKS public key from authorization server if s.Config.Server.Jwks.Uri != "" && tokenAuth == nil { @@ -84,19 +76,16 @@ func (s *Server) Serve() error { var err error tokenAuth, err = configurator.FetchPublicKeyFromURL(s.Config.Server.Jwks.Uri) if err != nil { - log.Error().Err(err).Msgf("failed to fetch JWKS") + logrus.Errorf("failed to fetch JWKS: %w", err) continue } break } } - // create client with opts to use to fetch data from SMD - opts := []client.Option{ - client.WithHost(s.Config.SmdClient.Host), - client.WithAccessToken(s.Config.AccessToken), - client.WithCertPoolFile(s.Config.CertPath), - } + // Setup logger + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + logger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) // create new go-chi router with its routes router := chi.NewRouter() @@ -115,17 +104,16 @@ func (s *Server) Serve() error { ) // protected routes if using auth - r.HandleFunc("/generate", s.Generate(opts...)) - r.Post("/targets", s.createTarget) + r.HandleFunc("/generate", s.Generate) + r.HandleFunc("/templates", s.ManageTemplates) }) } else { // public routes without auth - router.HandleFunc("/generate", s.Generate(opts...)) - router.Post("/targets", s.createTarget) + router.HandleFunc("/generate", s.Generate) + router.HandleFunc("/templates", s.ManageTemplates) } // always available public routes go here (none at the moment) - router.HandleFunc("/configurator/status", s.GetStatus) s.Handler = router return s.ListenAndServe() @@ -139,189 +127,51 @@ func (s *Server) Close() { // This is the corresponding service function to generate templated files, that // works similarly to the CLI variant. This function takes similiar arguments as // query parameters that are included in the HTTP request URL. -func (s *Server) Generate(opts ...client.Option) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // get all of the expect query URL params and validate - var ( - targetParam string = r.URL.Query().Get("target") - target *Target = s.getTarget(targetParam) - outputs generator.FileMap - err error - ) - s.GeneratorParams = parseGeneratorParams(r, target, opts...) - if targetParam == "" { - err = writeErrorResponse(w, "must specify a target") - log.Error().Err(err).Msg("failed to parse generator params") - return - } +func (s *Server) Generate(w http.ResponseWriter, r *http.Request) { + // get all of the expect query URL params and validate + s.GeneratorParams.Target = r.URL.Query().Get("target") + if s.GeneratorParams.Target == "" { + writeErrorResponse(w, "must specify a target") + return + } - // try to generate with target supplied by client first - if target != nil { - log.Debug().Any("target", target).Msg("target for Generate()") - outputs, err = generator.Generate(s.Config, target.PluginPath, s.GeneratorParams) - log.Debug().Any("outputs map", outputs).Msgf("after generate") - if err != nil { - log.Error().Err(err).Msg("failed to generate file") - return - } - } else { - // try and generate a new config file from supplied params - log.Debug().Str("target", targetParam).Msg("target for GenerateWithTarget()") - outputs, err = generator.GenerateWithTarget(s.Config, targetParam) - if err != nil { - writeErrorResponse(w, "failed to generate file") - log.Error().Err(err).Msgf("failed to generate file with target '%s'", target) - return - } - } - - // marshal output to JSON then send response to client - tmp := generator.ConvertContentsToString(outputs) - b, err := json.Marshal(tmp) - if err != nil { - writeErrorResponse(w, "failed to marshal output: %v", err) - log.Error().Err(err).Msg("failed to marshal output") - return - } - _, err = w.Write(b) - if err != nil { - writeErrorResponse(w, "failed to write response: %v", err) - log.Error().Err(err).Msg("failed to write response") - return - } - } -} - -func (s *Server) loadTargets() { - // make sure the map is initialized first - if s.Targets == nil { - s.Targets = make(map[string]Target) - } - // add default generator targets - for name, _ := range generator.DefaultGenerators { - serverTarget := Target{ - Name: name, - PluginPath: name, - } - s.Targets[name] = serverTarget - } - // add targets from config to server (overwrites default targets) - for name, target := range s.Config.Targets { - serverTarget := Target{ - Name: name, - } - // only overwrite plugin path if it's set - if target.Plugin != "" { - serverTarget.PluginPath = target.Plugin - } else { - serverTarget.PluginPath = name - } - // add templates using template paths from config - for _, templatePath := range target.TemplatePaths { - template := generator.Template{} - template.LoadFromFile(templatePath) - serverTarget.Templates = append(serverTarget.Templates, template) - } - s.Targets[name] = serverTarget - } -} - -func (s *Server) GetStatus(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - data := map[string]any{ - "code": 200, - "message": "Configurator is healthy", - } - err := json.NewEncoder(w).Encode(data) + // generate a new config file from supplied params + outputs, err := generator.GenerateWithTarget(s.Config, s.GeneratorParams) if err != nil { - fmt.Printf("failed to encode JSON: %v\n", err) + writeErrorResponse(w, "failed to generate file: %w", err) + return + } + + // marshal output to JSON then send response to client + tmp := generator.ConvertContentsToString(outputs) + b, err := json.Marshal(tmp) + if err != nil { + writeErrorResponse(w, "failed to marshal output: %w", err) + return + } + _, err = w.Write(b) + if err != nil { + writeErrorResponse(w, "failed to write response: %w", err) return } } -// Create a new target with name, generator, templates, and files. -// -// Example: -// -// curl -X POST /target?name=test&plugin=dnsmasq +// Incomplete WIP function for managing templates remotely. There is currently no +// internal API to do this yet. // // TODO: need to implement template managing API first in "internal/generator/templates" or something -func (s *Server) createTarget(w http.ResponseWriter, r *http.Request) { - var ( - target = Target{} - bytes []byte - err error - ) - if r == nil { - err = writeErrorResponse(w, "request is invalid") - log.Error().Err(err).Msg("request == nil") - return - } - - bytes, err = io.ReadAll(r.Body) +func (s *Server) ManageTemplates(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("this is not implemented yet")) if err != nil { - writeErrorResponse(w, "failed to read response body: %v", err) - log.Error().Err(err).Msg("failed to read response body") + writeErrorResponse(w, "failed to write response: %w", err) return } - defer r.Body.Close() - - err = json.Unmarshal(bytes, &target) - if err != nil { - writeErrorResponse(w, "failed to unmarshal target: %v", err) - log.Error().Err(err).Msg("failed to unmarshal target") - return - } - - // make sure a plugin and at least one template is supplied - if target.Name == "" { - err = writeErrorResponse(w, "target name is required") - log.Error().Err(err).Msg("set target as a URL query parameter") - return - } - if target.PluginPath == "" { - err = writeErrorResponse(w, "generator name is required") - log.Error().Err(err).Msg("must supply a generator name") - return - } - if len(target.Templates) <= 0 { - writeErrorResponse(w, "requires at least one template") - log.Error().Err(err).Msg("must supply at least one template") - return - } - - s.Targets[target.Name] = target - -} - -func (s *Server) getTarget(name string) *Target { - t, ok := s.Targets[name] - if ok { - return &t - } - return nil } // Wrapper function to simplify writting error message responses. This function // is only intended to be used with the service and nothing else. func writeErrorResponse(w http.ResponseWriter, format string, a ...any) error { errmsg := fmt.Sprintf(format, a...) - bytes, _ := json.Marshal(map[string]any{ - "level": "error", - "time": time.Now().Unix(), - "message": errmsg, - }) - http.Error(w, string(bytes), http.StatusInternalServerError) + w.Write([]byte(errmsg)) return fmt.Errorf(errmsg) } - -func parseGeneratorParams(r *http.Request, target *Target, opts ...client.Option) generator.Params { - var params = generator.Params{ - ClientOpts: opts, - Templates: make(map[string]generator.Template, len(target.Templates)), - } - for i, template := range target.Templates { - params.Templates[fmt.Sprintf("%s_%d", target.Name, i)] = template - } - return params -} diff --git a/pkg/util/params.go b/pkg/util/params.go index 322d852..6b342cd 100644 --- a/pkg/util/params.go +++ b/pkg/util/params.go @@ -11,7 +11,7 @@ type Params map[string]any type Option func(Params) // Extract all parameters from the options passed as map[string]any. -func ToDict(opts ...Option) Params { +func GetParams(opts ...Option) Params { params := Params{} for _, opt := range opts { opt(params) @@ -45,8 +45,8 @@ func WithDefault[T any](v T) Option { } } -// Sugary generic function to get parameter from util.Params. -func Get[T any](params Params, key string) *T { +// Syntactic sugar generic function to get parameter from util.Params. +func Get[T any](params Params, key string, opts ...Option) *T { if v, ok := params[key].(T); ok { return &v } @@ -55,16 +55,3 @@ func Get[T any](params Params, key string) *T { } return nil } - -func GetOpt[T any](opts []Option, key string) *T { - return Get[T](ToDict(opts...), "required_claims") -} - -func (p Params) GetVerbose() bool { - if verbose, ok := p["verbose"].(bool); ok { - return verbose - } - - // default setting - return false -} diff --git a/pkg/util/util.go b/pkg/util/util.go index 6ff13b0..fd0daa2 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -1,17 +1,13 @@ package util import ( - "archive/tar" "bytes" - "cmp" - "compress/gzip" "crypto/tls" "fmt" "io" "net/http" "os" "os/exec" - "slices" "strings" ) @@ -32,7 +28,7 @@ func IsDirectory(path string) (bool, error) { // This returns an *os.FileInfo type fileInfo, err := os.Stat(path) if err != nil { - return false, fmt.Errorf("failed to stat path (%s): %v", path, err) + return false, fmt.Errorf("failed to stat path: %v", err) } // IsDir is short for fileInfo.Mode().IsDir() @@ -67,7 +63,7 @@ func MakeRequest(url string, httpMethod string, body []byte, headers map[string] // NOTE: This currently requires git to be installed. // TODO: Change how this is done to not require executing a command. func GitCommit() string { - c := exec.Command("git", "rev-parse", "--short=8", "HEAD") + c := exec.Command("git", "rev-parse", "HEAD") stdout, err := c.Output() if err != nil { return "" @@ -84,11 +80,6 @@ func RemoveIndex[T comparable](s []T, index int) []T { return append(ret, s[index+1:]...) } -func RemoveDuplicates[T cmp.Ordered](s []T) []T { - slices.Sort(s) - return slices.Compact(s) -} - // General function to copy elements from slice if condition is true. func CopyIf[T comparable](s []T, condition func(t T) bool) []T { var f = make([]T, 0) @@ -99,64 +90,3 @@ func CopyIf[T comparable](s []T, condition func(t T) bool) []T { } return f } - -func CreateArchive(files []string, buf io.Writer) error { - // Create new Writers for gzip and tar - // These writers are chained. Writing to the tar writer will - // write to the gzip writer which in turn will write to - // the "buf" writer - gw := gzip.NewWriter(buf) - defer gw.Close() - tw := tar.NewWriter(gw) - defer tw.Close() - - // Iterate over files and add them to the tar archive - for _, file := range files { - err := addToArchive(tw, file) - if err != nil { - return err - } - } - - return nil -} - -func addToArchive(tw *tar.Writer, filename string) error { - // open file to write to archive - file, err := os.Open(filename) - if err != nil { - return err - } - defer file.Close() - - // get FileInfo for file size, mode, etc. - info, err := file.Stat() - if err != nil { - return err - } - - // create a tar Header from the FileInfo data - header, err := tar.FileInfoHeader(info, info.Name()) - if err != nil { - return err - } - - // use full path as name (FileInfoHeader only takes the basename) to - // preserve directory structure - // see for more info: https://golang.org/src/archive/tar/common.go?#L626 - header.Name = filename - - // Write file header to the tar archive - err = tw.WriteHeader(header) - if err != nil { - return err - } - - // copy file content to tar archive - _, err = io.Copy(tw, file) - if err != nil { - return err - } - - return nil -} diff --git a/res/archlinux/PKGBUILD b/res/archlinux/PKGBUILD deleted file mode 100644 index 56159b3..0000000 --- a/res/archlinux/PKGBUILD +++ /dev/null @@ -1,34 +0,0 @@ -# Maintainer: David J. Allen -pkgname=configurator -pkgver=v0.1.0alpha -pkgrel=1 -pkgdesc="An extensible tool to dynamically generate config files from SMD with Jinja 2 templating support." -arch=("x86_64") -url="https://github.com/OpenCHAMI/configurator" -license=('MIT') -groups=("openchami") -provides=('configurator') -conflicts=('configurator') -https://github.com/OpenCHAMI/configurator/releases/download/v0.1.0-alpha/configurator -source_x86_64=( - "${url}/releases/download/v0.1.0-alpha/${pkgname}.tar.gz" -) -sha256sums_x86_64=('28e10f1e39757bbdc3a503de74dd4d8c610d9c78e89665fb42012e8ef7834d0f') - -# we don't need to set pkgver just yet for the pre-release version... -# pkgver() { -# cd "$srcdir" || exit -# printf "%s" "$(git describe --tags --abbrev=0)" -# } - -package() { - cd "$srcdir/" || exit - - # install the binary to /usr/bin - mkdir -p "${pkgdir}/usr/bin" - mkdir -p "${pkgdir}/usr/lib/${pkgname}" - install -m755 configurator "${pkgdir}/usr/bin/configurator" - - # install plugins to /usr/lib - install -m755 *.so "${pkgdir}/usr/lib/${pkgname}" -} diff --git a/tests/generate_local.hurl b/tests/generate_local.hurl deleted file mode 100644 index d198e46..0000000 --- a/tests/generate_local.hurl +++ /dev/null @@ -1,22 +0,0 @@ -## -## Run these tests after starting server with `configurator serve...` -## - -# Generate a `example` config with default plugin and template -GET http://127.0.0.1:3334/generate?target=example -HTTP 200 - -# Create a new target using the API -POST http://127.0.0.1:3334/targets -{ - "name": "test", - "plugin": "example", - "templates": [{ - "contents": "This is an example template used with the example plugin." - }] -} -HTTP 200 - -# Test the new target just add from POST above -GET http://127.0.0.1:3334/generate?target=example -HTTP 200 \ No newline at end of file diff --git a/tests/generate_test.go b/tests/generate_test.go index 21ff978..39ba06e 100644 --- a/tests/generate_test.go +++ b/tests/generate_test.go @@ -3,26 +3,17 @@ package tests import ( "encoding/json" "fmt" - "log" "net/http" "os" "os/exec" - "path/filepath" "testing" configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/configurator/pkg/server" "github.com/OpenCHAMI/configurator/pkg/util" ) -var ( - workDir string - replaceDir string - err error -) - // A valid test generator that implements the `Generator` interface. type TestGenerator struct{} @@ -31,96 +22,98 @@ func (g *TestGenerator) GetVersion() string { return "v1.0.0" } func (g *TestGenerator) GetDescription() string { return "This is a plugin created for running tests." } -func (g *TestGenerator) Generate(config *config.Config, params generator.Params) (generator.FileMap, error) { +func (g *TestGenerator) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { // Jinja 2 template file - files := map[string]generator.Template{ - "test1": generator.Template{ - Contents: []byte(` + files := [][]byte{ + []byte(` Name: {{plugin_name}} Version: {{plugin_version}} Description: {{plugin_description}} This is the first test template file. - `), - }, - "test2": generator.Template{ - Contents: []byte(` + `), + []byte(` This is another testing Jinja 2 template file using {{plugin_name}}. - `), - }, + `), } // apply Jinja templates to file - fileMap, err := generator.ApplyTemplates(generator.Mappings{ + fileList, err := generator.ApplyTemplates(generator.Mappings{ "plugin_name": g.GetName(), "plugin_version": g.GetVersion(), "plugin_description": g.GetDescription(), - }, files) + }, files...) if err != nil { return nil, fmt.Errorf("failed to apply templates: %v", err) } + // make sure we're able to receive certain arguments when passed + params := generator.GetParams(opts...) + if len(params) <= 0 { + return nil, fmt.Errorf("expect at least one params, but found none") + } + // make sure we have a valid config we can access if config == nil { return nil, fmt.Errorf("invalid config (config is nil)") } + // make sure we're able to get a valid client as well + client := generator.GetClient(params) + if client == nil { + return nil, fmt.Errorf("invalid client (client is nil)") + } + // TODO: make sure we can get a target // make sure we have the same number of files in file list - var ( - fileInputCount = len(files) - fileOutputCount = len(fileMap) - ) - if fileInputCount != fileOutputCount { - return nil, fmt.Errorf("file output count (%d) is not the same as the input (%d)", fileOutputCount, fileInputCount) + if len(files) != len(fileList) { + return nil, fmt.Errorf("file list output count is not the same as the input") + } + + // convert file list to file map + fileMap := make(generator.FileMap, len(fileList)) + for i, contents := range fileList { + fileMap[fmt.Sprintf("t-%d.txt", i)] = contents } return fileMap, nil } -func init() { - workDir, err = os.Getwd() - if err != nil { - log.Fatalf("failed to get working directory: %v", err) - } - replaceDir = fmt.Sprintf("%s", filepath.Dir(workDir)) -} - // Test building and loading plugins func TestPlugin(t *testing.T) { var ( testPluginDir = t.TempDir() testPluginPath = fmt.Sprintf("%s/test-plugin.so", testPluginDir) testPluginSourcePath = fmt.Sprintf("%s/test-plugin.go", testPluginDir) - testPluginSource = []byte( - `package main + testPluginSource = []byte(` +package main import ( - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "github.com/OpenCHAMI/configurator/pkg" "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/util" ) type TestGenerator struct{} -func (g *TestGenerator) GetName() string { return "test" } -func (g *TestGenerator) GetVersion() string { return "v1.0.0" } -func (g *TestGenerator) GetDescription() string { - return "This is a plugin creating for running tests." -} -func (g *TestGenerator) Generate(config *config.Config, params generator.Params) (generator.FileMap, error) { +func (g *TestGenerator) GetName() string { return "test" } +func (g *TestGenerator) GetVersion() string { return "v1.0.0" } +func (g *TestGenerator) GetDescription() string { return "This is a plugin creating for running tests." } +func (g *TestGenerator) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { return generator.FileMap{"test": []byte("test")}, nil } - -var Generator TestGenerator`) +var Generator TestGenerator + `) ) - // get directory to replace remote pkg with local - // _, filename, _, _ := runtime.Caller(0) - // replaceDir := fmt.Sprintf("%s", filepath.Dir(workDir)) + wd, err := os.Getwd() + if err != nil { + t.Errorf("failed to get working directory: %v", err) + } // show all paths to make sure we're using the correct ones - fmt.Printf("(TestPlugin) working directory: %v\n", workDir) + fmt.Printf("(TestPlugin) working directory: %v\n", wd) fmt.Printf("(TestPlugin) plugin directory: %v\n", testPluginDir) fmt.Printf("(TestPlugin) plugin path: %v\n", testPluginPath) fmt.Printf("(TestPlugin) plugin source path: %v\n", testPluginSourcePath) @@ -158,12 +151,6 @@ var Generator TestGenerator`) t.Fatalf("failed to execute command: %v\n%s", err, string(output)) } - // use the local `pkg` instead of the release one - cmd = exec.Command("bash", "-c", fmt.Sprintf("go mod edit -replace=github.com/OpenCHAMI/configurator=%s", replaceDir)) - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to execute command: %v\n%s", err, string(output)) - } - // run `go mod tidy` for dependencies cmd = exec.Command("bash", "-c", "go mod tidy") if output, err := cmd.CombinedOutput(); err != nil { @@ -199,7 +186,7 @@ var Generator TestGenerator`) GetName() string GetVersion() string GetDescription() string - Generate(*config.Config, generator.Params) (generator.FileMap, error) + Generate(*configurator.Config, ...util.Option) (generator.FileMap, error) }); !ok { t.Error("plugin does not implement all of the generator interface") } @@ -216,7 +203,7 @@ var Generator TestGenerator`) GetName() string GetVersion() string GetDescription() string - Generate(*config.Config, generator.Params) (generator.FileMap, error) + Generate(*configurator.Config, ...util.Option) (generator.FileMap, error) }); !ok { t.Error("plugin does not implement all of the generator interface") } @@ -246,15 +233,16 @@ var Generator InvalidGenerator `) ) + wd, err := os.Getwd() + if err != nil { + t.Errorf("failed to get working directory: %v", err) + } // show all paths to make sure we're using the correct ones - fmt.Printf("(TestPluginWithInvalidOrNoSymbol) working directory: %v\n", workDir) + fmt.Printf("(TestPluginWithInvalidOrNoSymbol) working directory: %v\n", wd) fmt.Printf("(TestPluginWithInvalidOrNoSymbol) plugin directory: %v\n", testPluginDir) fmt.Printf("(TestPluginWithInvalidOrNoSymbol) plugin path: %v\n", testPluginPath) fmt.Printf("(TestPluginWithInvalidOrNoSymbol) plugin source path: %v\n", testPluginSourcePath) - // get directory to replace remote pkg with local - // _, filename, _, _ := runtime.Caller(0) - // make temporary directory to test plugin err = os.MkdirAll(testPluginDir, os.ModeDir) if err != nil { @@ -288,12 +276,6 @@ var Generator InvalidGenerator t.Fatalf("failed to execute command: %v\n%s", err, string(output)) } - // use the local `pkg` instead of the release one - cmd = exec.Command("bash", "-c", fmt.Sprintf("go mod edit -replace=github.com/OpenCHAMI/configurator=%s", replaceDir)) - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to execute command: %v\n%s", err, string(output)) - } - // run `go mod tidy` for dependencies cmd = exec.Command("bash", "-c", "go mod tidy") if output, err := cmd.CombinedOutput(); err != nil { @@ -332,8 +314,9 @@ var Generator InvalidGenerator // we're not doing it here since that's not what is being tested. func TestGenerateExample(t *testing.T) { var ( - conf = config.New() - gen = TestGenerator{} + config = configurator.NewConfig() + client = configurator.NewSmdClient() + gen = TestGenerator{} ) // make sure our generator returns expected strings @@ -350,7 +333,11 @@ func TestGenerateExample(t *testing.T) { }) // try to generate a file with templating applied - fileMap, err := gen.Generate(&conf, generator.Params{}) + fileMap, err := gen.Generate( + &config, + generator.WithTarget("test"), + generator.WithClient(client), + ) if err != nil { t.Fatalf("failed to generate file: %v", err) } @@ -369,7 +356,8 @@ func TestGenerateExample(t *testing.T) { // try and load the plugin from a lib here either. func TestGenerateExampleWithServer(t *testing.T) { var ( - conf = config.New() + config = configurator.NewConfig() + client = configurator.NewSmdClient() gen = TestGenerator{} headers = make(map[string]string, 0) ) @@ -377,14 +365,16 @@ func TestGenerateExampleWithServer(t *testing.T) { // NOTE: Currently, the server needs a config to know where to get load plugins, // and how to handle targets/templates. This will be simplified in the future to // decoupled the server from required a config altogether. - conf.Targets["test"] = configurator.Target{ + config.Targets["test"] = configurator.Target{ TemplatePaths: []string{}, FilePaths: []string{}, } // create new server, add test generator, and start in background - server := server.New(&conf) - generator.DefaultGenerators["test"] = &gen + server := server.New(&config) + server.GeneratorParams.Generators = map[string]generator.Generator{ + "test": &gen, + } go server.Serve() // make request to server to generate a file @@ -400,7 +390,10 @@ func TestGenerateExampleWithServer(t *testing.T) { // // NOTE: we don't actually use the config in this plugin implementation, // but we do check that a valid config was passed. - fileMap, err := gen.Generate(&conf, generator.Params{}) + fileMap, err := gen.Generate( + &config, + generator.WithClient(client), + ) if err != nil { t.Fatalf("failed to generate file: %v", err) }