Compare commits
90 commits
d7100cefe7
...
8960c2478a
| Author | SHA1 | Date | |
|---|---|---|---|
| 8960c2478a | |||
| 277de43a02 | |||
| bdd85b01ff | |||
| 2112e7eefd | |||
| eac73ada69 | |||
| c799dc7838 | |||
| c9f40e3857 | |||
| 8e1fa3d2ab | |||
| cb3d4ce8db | |||
| e115319913 | |||
| fa8ef7ab4b | |||
| afc7db53e1 | |||
| c495d10aad | |||
| 13b02c03e8 | |||
| fbed466c3d | |||
| d88ab2c01f | |||
| ebb95a29ec | |||
| 5d350717f4 | |||
| 18edb93d2c | |||
| 32d534bcea | |||
| 0349ecaf34 | |||
| d191577ac9 | |||
| 4771bf45ac | |||
| 7fa685e862 | |||
| dc8a9cff20 | |||
| b791b84890 | |||
| 73498a08de | |||
| 0f6f8957f6 | |||
| 4d96010199 | |||
| 947fbba854 | |||
| 94887aae9e | |||
| 325f77b9d4 | |||
| 418889b17f | |||
| 85e333289b | |||
| e458783061 | |||
| a4d1de9a51 | |||
| 036bda61b9 | |||
| ac36201f07 | |||
| e2b400fb12 | |||
| dc6818e1e2 | |||
| 08a9b9bdcf | |||
| b18746957d | |||
| cdc441344f | |||
| d1e7b275a6 | |||
| f917d2b6f8 | |||
| 135245ca9c | |||
| df8730463e | |||
| 3244a66f8e | |||
| e5c1b59bc1 | |||
| c2d5be5eed | |||
| f897bc3ca5 | |||
| 2536848541 | |||
| 98f9acad5d | |||
| 1ebea8cb73 | |||
| fbdaf218eb | |||
| 134a0dcac0 | |||
| eb126d5350 | |||
| 8b161135ff | |||
| 86f37555b2 | |||
| 59a5225b28 | |||
| 7a96bfd6c7 | |||
| 2d6eb1d972 | |||
| 0de7beefd0 | |||
| ea4819e97a | |||
| c24bcf34d4 | |||
| 5767a8fd47 | |||
| a8e9ed95e6 | |||
| 68d905067f | |||
| d7a0ddc1c2 | |||
| a54b39b296 | |||
| 2da5ca3702 | |||
| 5c4bbe0b58 | |||
| 4d33b12fe0 | |||
| 97fa0a1062 | |||
| d56a9e452f | |||
| 0d27f07a8b | |||
| fa949baafd | |||
| fe67674829 | |||
| 920703fc0e | |||
| 7e9e186138 | |||
| 419e9781bf | |||
| 1f5775196d | |||
| 05a4913e70 | |||
| a8c16ed715 | |||
| 1413312893 | |||
| a1a9c6407f | |||
| 72be62c78e | |||
| 50e6b53091 | |||
| ba684bd149 | |||
| bfd83f35a3 |
72 changed files with 4269 additions and 2901 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
**configurator
|
**configurator
|
||||||
|
**makeshift
|
||||||
**.yaml
|
**.yaml
|
||||||
**.yml
|
**.yml
|
||||||
**.so
|
**.so
|
||||||
|
|
@ -6,3 +7,7 @@
|
||||||
**.ignore
|
**.ignore
|
||||||
**.tar.gz
|
**.tar.gz
|
||||||
dist/
|
dist/
|
||||||
|
tests/data
|
||||||
|
tests/downloads
|
||||||
|
tests/profiles
|
||||||
|
tests/plugins
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ before:
|
||||||
- go mod download
|
- go mod download
|
||||||
- make plugins
|
- make plugins
|
||||||
builds:
|
builds:
|
||||||
- id: "configurator"
|
- id: "makeshift"
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
goarch:
|
goarch:
|
||||||
|
|
@ -30,10 +30,10 @@ archives:
|
||||||
dockers:
|
dockers:
|
||||||
-
|
-
|
||||||
image_templates:
|
image_templates:
|
||||||
- ghcr.io/openchami/{{.ProjectName}}:latest
|
- git.towk2.me/towk/{{.ProjectName}}:latest
|
||||||
- ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}
|
- git.towk2.me/towk/{{.ProjectName}}:{{ .Tag }}
|
||||||
- ghcr.io/openchami/{{.ProjectName}}:v{{ .Major }}
|
- git.towk2.me/towk/{{.ProjectName}}:v{{ .Major }}
|
||||||
- ghcr.io/openchami/{{.ProjectName}}:v{{ .Major }}.{{ .Minor }}
|
- git.towk2.me/towk/{{.ProjectName}}:v{{ .Major }}.{{ .Minor }}
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- "--pull"
|
- "--pull"
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
[0.1.0]
|
[0.1.0]
|
||||||
|
|
||||||
- Initial prerelease of configurator
|
- Initial prerelease of makeshift
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright © 2024 Triad National Security, LLC. This program was produced under U.S. Government contract 89233218CNA000001 for Los Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC for the U.S. Department of Energy/National Nuclear Security Administration.
|
Copyright © 2025 David J. Allen
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
copy of this software and associated documentation files (the "Software"),
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
|
|
||||||
10
Makefile
10
Makefile
|
|
@ -1,7 +1,7 @@
|
||||||
# Unless set otherwise, the container runtime is Docker
|
# Unless set otherwise, the container runtime is Docker
|
||||||
DOCKER ?= docker
|
DOCKER ?= docker
|
||||||
|
|
||||||
prog ?= configurator
|
prog ?= makeshift
|
||||||
git_tag := $(shell git describe --abbrev=0 --tags --always)
|
git_tag := $(shell git describe --abbrev=0 --tags --always)
|
||||||
sources := main.go $(wildcard cmd/*.go)
|
sources := main.go $(wildcard cmd/*.go)
|
||||||
plugin_source_prefix := pkg/generator/plugins
|
plugin_source_prefix := pkg/generator/plugins
|
||||||
|
|
@ -36,18 +36,18 @@ container-testing: binaries
|
||||||
plugins: $(plugin_binaries)
|
plugins: $(plugin_binaries)
|
||||||
|
|
||||||
# how to make each plugin
|
# how to make each plugin
|
||||||
lib/%.so: pkg/generator/plugins/%/*.go
|
lib/%.so: pkg/plugins/%/*.go
|
||||||
mkdir -p lib
|
mkdir -p lib
|
||||||
go build -buildmode=plugin -o $@ $<
|
go build -buildmode=plugin -o $@ $<
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
go doc github.com/OpenCHAMI/cmd
|
go doc git.towk2.me/towk/makeshift/cmd
|
||||||
go doc github.com/OpenCHAMI/pkg/configurator
|
go doc git.towk2.me/towk/makeshift/pkg/${prog}
|
||||||
|
|
||||||
# remove executable and all built plugins
|
# remove executable and all built plugins
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -f configurator
|
rm -f ${prog}
|
||||||
rm -f lib/*
|
rm -f lib/*
|
||||||
|
|
||||||
# run all of the unit tests
|
# run all of the unit tests
|
||||||
|
|
|
||||||
358
README.md
358
README.md
|
|
@ -1,219 +1,241 @@
|
||||||
# Configurator: Next Generation
|
# << Makeshift >>
|
||||||
|
|
||||||
The `configurator` is general-purpose FTP-like service with rendering capabilities using Jinja 2 templating. It is designed with managing configuration files across a fleet of systems in mind and provides flexible mechanisms for populating template mappings.
|
The `makeshift` tool is a service that serves files and CLI that downloads them with a couple of handy features baked-in. Although the CLI and server component function more like a glorified FTP, the power of this tool comes from the plugin system. For example, the file cobbler is built to run external plugins for more advanced processing files before serving them (e.g. fetching from a data source, rendering Jinja 2 templates, etc.).
|
||||||
|
|
||||||
## 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:
|
## Building and Go!
|
||||||
|
|
||||||
|
|
||||||
|
The `makeshift` tool is built using standard `go` build tools. To get started, clone the project, download the dependencies, and build the project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/OpenCHAMI/configurator.git
|
git clone https://git.towk2.me/towk/makeshift.git
|
||||||
go mod tidy
|
go mod tidy
|
||||||
go build --tags all # equivalent to `go build --tags client,server``
|
go build
|
||||||
```
|
```
|
||||||
|
|
||||||
This will build the main driver program with the default generators that are found in the `pkg/generators` directory.
|
This will build the main driver program with the default generators that are found in the `pkg/generators` directory.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!NOTE]
|
||||||
> Not all of the plugins have completed generation implementations and are a WIP.
|
> The project does not current separate the client, server, and plugin components using build tags, but will eventually. This will allow users to only compile and distribute specific parts of the tool with limited functionality.
|
||||||
|
|
||||||
### Running Configurator with CLI
|
|
||||||
|
|
||||||
After you build the program, run the following command to use the tool:
|
## Basic Examples
|
||||||
|
|
||||||
|
Here are some of the common commands you may want to try right off the bat (aside from `makeshift help` of course). The commands below that do not include the `--host`, `--path`, or `--root` flags are set using the environment variables.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIs...
|
export MAKESHIFT_HOST=localhost
|
||||||
./configurator generate --config config.yaml --target coredhcp -o coredhcp.conf --cacert ochami.pem
|
export MAKESHIFT_PATH=/test
|
||||||
|
export MAKESHIFT_ROOT=./test
|
||||||
|
export MAKESHIFT_LOG_FILE=logs/makeshift.log
|
||||||
|
export MAKESHIFT_LOG_LEVEL=debug
|
||||||
```
|
```
|
||||||
|
|
||||||
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).
|
Start the server. The `--init` flag with create the default files and directory to get started at the `--root` path.
|
||||||
|
|
||||||
In other words, there should be an entry in the config file that looks like this:
|
```bash
|
||||||
|
makeshift serve --root $HOME/apps/makeshift/server --init
|
||||||
|
```
|
||||||
|
|
||||||
```yaml
|
From here, you might want to see what files are available by default.
|
||||||
...
|
|
||||||
targets:
|
|
||||||
coredhcp:
|
|
||||||
plugin: "lib/coredhcp.so" # optional, if we want to use an external plugin instead
|
|
||||||
templates:
|
|
||||||
- templates/coredhcp.j2
|
|
||||||
...
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# list the files in the root directory
|
||||||
|
makeshift list
|
||||||
|
|
||||||
|
# list files store in the template directory with a specified host
|
||||||
|
makeshift list --host http://localhost:5050 --path templates
|
||||||
|
|
||||||
|
# list all available plugins
|
||||||
|
makeshift list plugins
|
||||||
|
|
||||||
|
# list specific plugin (same as 'makeshift plugins info jinja2')
|
||||||
|
makeshift list plugins jinja2
|
||||||
|
|
||||||
|
# list all available profiles
|
||||||
|
makeshift list profiles
|
||||||
|
|
||||||
|
# list specific profile information
|
||||||
|
makeshift list profiles default
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, we can start downloading some files or directories (as archives).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# download all data (notice --host and --port are not set here)
|
||||||
|
makeshift download
|
||||||
|
|
||||||
|
# download the 'help.txt' file without processing (i.e. using plugins)
|
||||||
|
makeshift download --host http://localhost:5050 --path help.txt
|
||||||
|
|
||||||
|
# download files with rendering using Jinja 2 plugin and default profile
|
||||||
|
makeshift download -p help.txt --plugins jinja2 --profile default
|
||||||
|
|
||||||
|
# download directory with rendering using plugins to fetch data and render
|
||||||
|
# using a custom 'compute' profile
|
||||||
|
makeshift download -p templates --plugins smd,jinja2 --profile compute
|
||||||
|
|
||||||
|
# do everything in the above example but extract and remove archive
|
||||||
|
makeshift download -p templates --plugins smd,jinja2 --profile compute -xr
|
||||||
|
|
||||||
|
# download a raw plugin
|
||||||
|
makeshift download plugin jinja2
|
||||||
|
|
||||||
|
# download a profile
|
||||||
|
makeshift download profile default
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The `configurator` tool requires a valid access token when making requests to an instance of SMD that has protected routes.
|
> Plugins are ran in order specified with the `--plugins` flag, which means if you're creating a plugin to write to a data store and then read in a subsequent plugin, the order specified with the CLI matters!
|
||||||
|
|
||||||
### Running Configurator as a Service
|
(WIP) Files, directories, profiles, and plugins will eventually be able to be uploaded to the server.
|
||||||
|
|
||||||
The tool can also run as a service to generate files for clients:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export CONFIGURATOR_JWKS_URL="http://my.openchami.cluster:8443/key"
|
# upload a single file in root directory
|
||||||
./configurator serve --config config.yaml
|
makeshift upload -d @compute-base.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:
|
# upload a directory (not working yet...)
|
||||||
|
makeshift upload -d @setup/
|
||||||
|
|
||||||
```bash
|
# upload an archive (extracted and saved on server - not working yet...)
|
||||||
export ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIs...
|
makeshift upload -d @setup.tar.gz -t archive
|
||||||
curl http://127.0.0.1:3334/generate?target=coredhcp -X GET -H "Authorization: Bearer $ACCESS_TOKEN" --cacert ochami.pem
|
|
||||||
# ...or...
|
|
||||||
./configurator fetch --target coredhcp --host http://127.0.0.1:3334 --cacert ochami.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
# upload a new profile
|
||||||
|
makeshift upload profile -d @compute.json kubernetes.json
|
||||||
|
|
||||||
### Docker
|
# upload a new profile with a specific path
|
||||||
|
makeshift upload profile -d @kubernetes.json
|
||||||
|
makeshift upload profile -d '{"id": "custom", "data": {}}' kubernetes.json
|
||||||
|
|
||||||
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.
|
# upload a new plugin
|
||||||
|
makeshift upload plugin -d @slurm.so
|
||||||
```bash
|
makeshift upload plugin slurm.so
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// maps the file path to its contents
|
|
||||||
type FileMap = map[string][]byte
|
|
||||||
|
|
||||||
// interface for generator plugins
|
|
||||||
type Generator interface {
|
|
||||||
GetName() string
|
|
||||||
GetVersion() string
|
|
||||||
GetDescription() string
|
|
||||||
Generate(config *configurator.Config, opts ...util.Option) (FileMap, 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.
|
|
||||||
|
|
||||||
```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{ /*...*/ }
|
|
||||||
|
|
||||||
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"]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *MyGenerator) GetVersion() string {
|
|
||||||
return g.PluginInfo["version"] // "v1.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *MyGenerator) GetDescription() string {
|
|
||||||
return g.PluginInfo["description"] // "This is an example plugin."
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *MyGenerator) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) {
|
|
||||||
// do config generation stuff here...
|
|
||||||
var (
|
|
||||||
params = generator.GetParams(opts...)
|
|
||||||
client = generator.GetClient(params)
|
|
||||||
output = ""
|
|
||||||
)
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this MUST be named "Generator" for symbol lookup in main driver
|
|
||||||
var Generator MyGenerator
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The keys in `generator.ApplyTemplate` must not contain illegal characters such as a `-` or else the templates will not apply correctly.
|
> Although every command has a `curl` equivalent, it is better to use the CLI since it has other features such as extracting and remove archives after downloading and saving archives as files automatically.
|
||||||
|
|
||||||
Finally, build the plugin and put it somewhere specified by `plugins` in your config. Make sure that the package is `main` before building.
|
## Server Root Structure
|
||||||
|
|
||||||
|
The `makeshift` server serves files at the specified `--root` path (also set with `MAKESHIFT_ROOT` environment variable). The directory structure looks like the following by default with initializing with `makeshift init $MAKESHIFT_ROOT`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -buildmode=plugin -o lib/mygenerator.so path/to/mygenerator.go
|
├── data
|
||||||
|
├── plugins
|
||||||
|
└── profiles
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
Each directory holds specific files for different purposes:
|
||||||
|
|
||||||
|
- `data` - Stores any and all miscellaenous files and directories.
|
||||||
|
- `plugins` - Stores plugins defined in the ["Creating Plugins"](#creating-plugins) section.
|
||||||
|
- `profiles` - Stores profiles in JSON format as defined in the ["Creating Profiles"](#creating-profiles) section.
|
||||||
|
|
||||||
|
|
||||||
|
## Creating Plugins
|
||||||
|
|
||||||
|
The `makeshift` tool defines a plugin as an interface that can be implemented and compiled.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Plugin interface {
|
||||||
|
Name() string
|
||||||
|
Version() string
|
||||||
|
Description() string
|
||||||
|
Metadata() Metadata
|
||||||
|
|
||||||
|
Init() error
|
||||||
|
Run(data storage.KVStore, args []string) error
|
||||||
|
Cleanup() error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugins can *literally* contain whatever you want and is written in Go. Here is a simple example implementation to demonstrate how that is done which we will save at `src/example.go`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Example struct{}
|
||||||
|
|
||||||
|
func (p *Example) Name() string { return "example" }
|
||||||
|
func (p *Example) Version() string { return "v0.0.1-alpha" }
|
||||||
|
func (p *Example) Description() string { return "An example plugin" }
|
||||||
|
func (p *Example) Metadata() map[string]string {
|
||||||
|
return makeshift.Metadata{
|
||||||
|
"author": map[string]any{
|
||||||
|
"name": "John Smith",
|
||||||
|
"email": "john.smith@example",
|
||||||
|
"links": []string{
|
||||||
|
"https://example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Example) Init() error {
|
||||||
|
// Initialize the plugin if necessary.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Example) Run(data storage.KVStore, args []string) error {
|
||||||
|
// Plugins can read and write to a data stores passed in.
|
||||||
|
// See the 'jinja2' plugin for reading and 'smd' plugin for writing.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Example) Clean() error {
|
||||||
|
// Clean up resources if necessary.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This MUST be included to find the symbol in the main driver executable.
|
||||||
|
var Makeshift Example
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, we can use the built-in `makeshift plugins compile` command to compile it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
makeshift plugins compile src/example.go -o $MAKESHIFT_ROOT/plugins/example.so
|
||||||
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> See the `examples/test.go` file for a plugin and template example.
|
> Make sure you move all of your plugins to `$MAKESHIFT_ROOT/plugins` to use them and should have an `*.so` name for lookup. For example, to use a custom plugin with `makeshift download -p templates/hosts.j2 --plugins my-plugin`, there has to a plugin `$MAKESHIFT_ROOT/plugins/my-plugin.so`.
|
||||||
|
|
||||||
## Configuration
|
## Creating Profiles
|
||||||
|
|
||||||
Here is an example config file to start using configurator:
|
On the other hand, profiles are simply objects that contain data used to populate data stores. The `makeshift` tool does not currently use all fields of a profile which will likely be removed in the near future.
|
||||||
|
|
||||||
```yaml
|
```go
|
||||||
server: # Server-related parameters when using as service
|
type Profile struct {
|
||||||
host: 127.0.0.1
|
ID string `json:"id"` // profile ID
|
||||||
port: 3334
|
Description string `json:"description,omitempty"` // profile description
|
||||||
jwks: # Set the JWKS uri for protected routes
|
Tags []string `json:"tags,omitempty"` // tags used for filtering (not implemented yet)
|
||||||
uri: ""
|
Data map[string]any `json:"data,omitempty"` // include render data
|
||||||
retries: 5
|
}
|
||||||
smd: # SMD-related parameters
|
|
||||||
host: http://127.0.0.1:27779
|
|
||||||
plugins: # path to plugin directories
|
|
||||||
- "lib/"
|
|
||||||
targets: # targets to call with --target flag
|
|
||||||
coredhcp:
|
|
||||||
templates:
|
|
||||||
- templates/coredhcp.j2
|
|
||||||
files: # files to be copied without templating
|
|
||||||
- extra/nodes.conf
|
|
||||||
targets: # additional targets to run (does not run recursively)
|
|
||||||
- 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.
|
Profiles can be created using JSON and only require an `id` with optional `data`. See the example in `$MAKESHIFT_ROOT/profiles/default.json`.
|
||||||
|
|
||||||
## Running the Tests
|
```json
|
||||||
|
{
|
||||||
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:
|
"id": "default",
|
||||||
|
"description": "Makeshift default profile",
|
||||||
```bash
|
"data": {
|
||||||
go test ./tests/generate_test.go --tags=all
|
"host": "localhost",
|
||||||
|
"path": "/test",
|
||||||
|
"server_root": "./test"
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Known Issues
|
> [!TIP]
|
||||||
|
> Make sure that you store your custom profiles in `$MAKESHIFT_ROOT/profiles` and that you set the name you want to use for lookup with a `*.json` extension (e.g. `compute.json`).
|
||||||
|
|
||||||
- Adds a new `OAuthClient` with every token request
|
## TODO: Missing Features
|
||||||
- Plugins are being loaded each time a file is generated
|
|
||||||
|
|
||||||
## TODO
|
There are some features still missing that will be added later.
|
||||||
|
|
||||||
- Add group functionality to create by files by groups
|
1. Running `makeshift` locally with profiles and plugins
|
||||||
- Extend SMD client functionality (or make extensible?)
|
2. Plugin to add user data for one-time use without creating a profile
|
||||||
- Handle authentication with `OAuthClient`'s correctly
|
3. Optionally build plugins directly into the main driver
|
||||||
|
4. Protected routes that require authentication
|
||||||
|
5. Configuration file for persistent runs
|
||||||
|
6. `Dockerfile` and `docker-compose.yml` files to build containers
|
||||||
26
bin/compile-plugins.sh
Executable file
26
bin/compile-plugins.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/usr/bin bash
|
||||||
|
|
||||||
|
|
||||||
|
function compile_default_plugins() {
|
||||||
|
makeshift_exe=./makeshift
|
||||||
|
go_exe=go
|
||||||
|
|
||||||
|
# make sure go build tools are installed
|
||||||
|
if command -v $go_exe >/dev/null 2>&1; then
|
||||||
|
# make sure that MAKESHIFT_ROOT is set
|
||||||
|
if [[ ! -v MAKESHIFT_ROOT ]]; then
|
||||||
|
# Compile the default external plugins
|
||||||
|
go build
|
||||||
|
$makeshift_exe compile pkg/plugins/jinja2/jinja2.go -o $MAKESHIFT_ROOT/plugins/jinja2.go
|
||||||
|
$makeshift_exe compile pkg/plugins/smd/smd.go -o $MAKESHIFT_ROOT/plugins/smd.so
|
||||||
|
$makeshift_exe compile pkg/plugins/userdata/userdata.go -o $MAKESHIFT_ROOT/plugins/userdata.go
|
||||||
|
else
|
||||||
|
echo "requires MAKESHIFT_ROOT to be set"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Go build tools must be installed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
compile_default_plugins
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
# Maintainer: David J. Allen <allend@lanl.gov>
|
# Maintainer: David J. Allen <davidallendj@gmail.com>
|
||||||
pkgname=configurator
|
pkgname=makeshift
|
||||||
pkgver=v0.1.0alpha
|
pkgver=v0.1.0alpha
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="An extensible tool to dynamically generate config files from SMD with Jinja 2 templating support."
|
pkgdesc="Extensible file cobbler"
|
||||||
arch=("x86_64")
|
arch=("x86_64")
|
||||||
url="https://github.com/OpenCHAMI/configurator"
|
url="https://git.towk2.me/towk/makeshift"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
groups=("openchami")
|
# groups=("towk")
|
||||||
provides=('configurator')
|
provides=('makeshift')
|
||||||
conflicts=('configurator')
|
conflicts=('makeshift')
|
||||||
https://github.com/OpenCHAMI/configurator/releases/download/v0.1.0-alpha/configurator
|
# https://git.towk2.me/towk/makeshift/releases/download/v0.1.0-alpha/makeshift
|
||||||
source_x86_64=(
|
source_x86_64=(
|
||||||
"${url}/releases/download/v0.1.0-alpha/${pkgname}.tar.gz"
|
"${url}/releases/download/v0.1.0-alpha/${pkgname}.tar.gz"
|
||||||
)
|
)
|
||||||
|
|
@ -27,7 +27,7 @@ package() {
|
||||||
# install the binary to /usr/bin
|
# install the binary to /usr/bin
|
||||||
mkdir -p "${pkgdir}/usr/bin"
|
mkdir -p "${pkgdir}/usr/bin"
|
||||||
mkdir -p "${pkgdir}/usr/lib/${pkgname}"
|
mkdir -p "${pkgdir}/usr/lib/${pkgname}"
|
||||||
install -m755 configurator "${pkgdir}/usr/bin/configurator"
|
install -m755 makeshift "${pkgdir}/usr/bin/makeshift"
|
||||||
|
|
||||||
# install plugins to /usr/lib
|
# install plugins to /usr/lib
|
||||||
install -m755 *.so "${pkgdir}/usr/lib/${pkgname}"
|
install -m755 *.so "${pkgdir}/usr/lib/${pkgname}"
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
var configCmd = &cobra.Command{
|
|
||||||
Use: "config",
|
|
||||||
Short: "Create a new default config file",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
// create a new config at all args (paths)
|
|
||||||
//
|
|
||||||
// TODO: change this to only take a single arg since more
|
|
||||||
// than one arg is *maybe* a mistake
|
|
||||||
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")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
config.SaveDefault(path)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(configCmd)
|
|
||||||
}
|
|
||||||
155
cmd/delete.go
Normal file
155
cmd/delete.go
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/client"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var deleteCmd = &cobra.Command{
|
||||||
|
Use: "delete",
|
||||||
|
Example: `
|
||||||
|
# set up environment
|
||||||
|
export MAKESHIFT_HOST=http://localhost:5050
|
||||||
|
export MAKESHIFT_PATH=test
|
||||||
|
|
||||||
|
# delete a file or directory (cannot delete root)
|
||||||
|
makeshift delete -p help.txt
|
||||||
|
makeshift delete --host http://localhost:5555 --path templates
|
||||||
|
`,
|
||||||
|
Short: "Delete files and directories",
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
setenv(cmd, "host", "MAKESHIFT_HOST")
|
||||||
|
setenv(cmd, "path", "MAKESHIFT_PATH")
|
||||||
|
setenv(cmd, "cacert", "MAKESHIFT_CACERT")
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
paths, _ = cmd.Flags().GetStringSlice("path")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("cacert", cacertPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
if path == "" {
|
||||||
|
log.Warn().Msg("skipping empty path")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
query = fmt.Sprintf("/delete/%s?", path)
|
||||||
|
res, _, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodDelete,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, query, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleteProfilesCmd = &cobra.Command{
|
||||||
|
Use: "profiles",
|
||||||
|
Example: `
|
||||||
|
# delete profile(s) by its ID
|
||||||
|
makeshift delete profiles kubernetes slurm compute
|
||||||
|
`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Short: "Delete profile(s)",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("cacert", cacertPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
for _, profileID := range args {
|
||||||
|
if profileID == "default" {
|
||||||
|
log.Warn().Msg("cannot delete the default profile")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
query = fmt.Sprintf("/profiles/%s", profileID)
|
||||||
|
res, _, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodDelete,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, query, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var deletePluginsCmd = &cobra.Command{
|
||||||
|
Use: "plugins",
|
||||||
|
Example: `
|
||||||
|
# delete plugin(s) by name
|
||||||
|
makeshift delete plugins weather slurm user
|
||||||
|
`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Short: "Delete plugin(s)",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("cacert", cacertPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pluginName := range args {
|
||||||
|
query = fmt.Sprintf("/plugins/%s", pluginName)
|
||||||
|
res, _, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodDelete,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, query, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
deleteCmd.PersistentFlags().String("host", "http://localhost:5050", "Set the makeshift server host (can be set with MAKESHIFT_HOST)")
|
||||||
|
deleteCmd.PersistentFlags().String("cacert", "", "Set the CA certificate path to load")
|
||||||
|
deleteCmd.Flags().StringSliceP("path", "p", []string{}, "Set the paths to delete files and directories")
|
||||||
|
deleteCmd.AddCommand(deleteProfilesCmd, deletePluginsCmd)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(deleteCmd)
|
||||||
|
}
|
||||||
305
cmd/download.go
Normal file
305
cmd/download.go
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.towk2.me/towk/makeshift/internal/archive"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/client"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var downloadCmd = cobra.Command{
|
||||||
|
Use: "download",
|
||||||
|
Example: `
|
||||||
|
# set up environment
|
||||||
|
export MAKESHIFT_HOST=http://localhost:5050
|
||||||
|
export MAKESHIFT_PATH=test
|
||||||
|
|
||||||
|
# download a file or directory (as archive)
|
||||||
|
makeshift download
|
||||||
|
makeshift download --host http://localhost:5050.com --path test
|
||||||
|
|
||||||
|
# download a file or directory and run plugins with profile data
|
||||||
|
makeshift download --plugins smd,jinja2 --profile compute
|
||||||
|
curl $MAKESHIFT_HOST/download/test?plugins=smd,jinja2&profile=test
|
||||||
|
|
||||||
|
# download directory and extract it's contents automatically
|
||||||
|
# then, remove the downloaded archive
|
||||||
|
makeshift download -xr
|
||||||
|
`,
|
||||||
|
Short: "Download and modify files with plugins",
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
setenv(cmd, "host", "MAKESHIFT_HOST")
|
||||||
|
setenv(cmd, "path", "MAKESHIFT_PATH")
|
||||||
|
setenv(cmd, "cacert", "MAKESHIFT_CACERT")
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
path, _ = cmd.Flags().GetString("path")
|
||||||
|
outputPath, _ = cmd.Flags().GetString("output")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
pluginNames, _ = cmd.Flags().GetStringSlice("plugins")
|
||||||
|
profileIDs, _ = cmd.Flags().GetStringSlice("profiles")
|
||||||
|
extract, _ = cmd.Flags().GetBool("extract")
|
||||||
|
removeArchive, _ = cmd.Flags().GetBool("remove-archive")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
query = fmt.Sprintf("/download/%s?", path)
|
||||||
|
if len(pluginNames) > 0 {
|
||||||
|
query += "plugins=" + url.QueryEscape(strings.Join(pluginNames, ","))
|
||||||
|
}
|
||||||
|
if len(profileIDs) > 0 {
|
||||||
|
query += "&profiles=" + url.QueryEscape(strings.Join(profileIDs, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", path).
|
||||||
|
Str("query", query).
|
||||||
|
Str("output", outputPath).
|
||||||
|
Strs("profiles", profileIDs).
|
||||||
|
Strs("plugins", pluginNames).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", path).
|
||||||
|
Str("output", outputPath).
|
||||||
|
Msg("failed to make request")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
log.Error().
|
||||||
|
Any("status", map[string]any{
|
||||||
|
"code": res.StatusCode,
|
||||||
|
"message": res.Status,
|
||||||
|
}).
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", path).
|
||||||
|
Str("output", outputPath).
|
||||||
|
Msg("response returned bad status")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine if output path is an archive or file
|
||||||
|
switch res.Header.Get("FILETYPE") {
|
||||||
|
case "archive":
|
||||||
|
// write archive to disk with or without '-o' specified
|
||||||
|
if outputPath == "" {
|
||||||
|
outputPath = fmt.Sprintf("%s.tar.gz", path)
|
||||||
|
writeFiles(outputPath, body)
|
||||||
|
log.Debug().Str("path", outputPath).Msg("wrote archive to pre-determined path")
|
||||||
|
} else {
|
||||||
|
writeFiles(outputPath, body)
|
||||||
|
log.Debug().Str("path", outputPath).Msg("wrote archive to specified path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract files if '-x' flag is passed
|
||||||
|
if extract {
|
||||||
|
var (
|
||||||
|
dir = filepath.Dir(outputPath)
|
||||||
|
base = strings.TrimSuffix(filepath.Base(outputPath), ".tar.gz")
|
||||||
|
)
|
||||||
|
err = archive.Expand(outputPath, fmt.Sprintf("%s/%s", dir, base))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("path", outputPath).
|
||||||
|
Msg("failed to expand archive")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionally, remove archive if '-r' flag is passed
|
||||||
|
// NOTE: this can only be used if `-x` flag is set
|
||||||
|
if removeArchive {
|
||||||
|
if !extract {
|
||||||
|
log.Warn().Msg("requires '-x/--extract' flag to be set to 'true'")
|
||||||
|
} else {
|
||||||
|
err = os.Remove(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("path", outputPath).
|
||||||
|
Msg("failed to remove archive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "file":
|
||||||
|
// write to file if '-o' specified otherwise stdout
|
||||||
|
if outputPath != "" {
|
||||||
|
writeFiles(outputPath, body)
|
||||||
|
log.Debug().Str("path", outputPath).Msg("wrote file to specified path")
|
||||||
|
} else {
|
||||||
|
fmt.Println(string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadProfileCmd = &cobra.Command{
|
||||||
|
Use: "profile",
|
||||||
|
Example: `
|
||||||
|
// download a profile
|
||||||
|
makeshift download profile default
|
||||||
|
`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Download a profile",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
outputPath, _ = cmd.Flags().GetString("output")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
body []byte
|
||||||
|
query string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("output", outputPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, profileID := range args {
|
||||||
|
query = fmt.Sprintf("/profiles/%s", profileID)
|
||||||
|
res, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Msg("failed to make request")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
log.Error().
|
||||||
|
Any("status", map[string]any{
|
||||||
|
"code": res.StatusCode,
|
||||||
|
"message": res.Status,
|
||||||
|
}).
|
||||||
|
Str("host", host).
|
||||||
|
Msg("response returned bad status")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if outputPath != "" {
|
||||||
|
writeFiles(outputPath, body)
|
||||||
|
} else {
|
||||||
|
fmt.Println(string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadPluginCmd = &cobra.Command{
|
||||||
|
Use: "plugin",
|
||||||
|
Example: `
|
||||||
|
// download a plugin
|
||||||
|
makeshift download plugin smd jinja2
|
||||||
|
`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Download a raw plugin",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
outputPath, _ = cmd.Flags().GetString("output")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("output", outputPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pluginName := range args {
|
||||||
|
query = fmt.Sprintf("/plugins/%s/raw", pluginName)
|
||||||
|
res, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Str("query", query).
|
||||||
|
Msg("failed to make request")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
log.Error().
|
||||||
|
Any("status", map[string]any{
|
||||||
|
"code": res.StatusCode,
|
||||||
|
"message": res.Status,
|
||||||
|
}).
|
||||||
|
Str("host", host).
|
||||||
|
Msg("response returned bad status")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if outputPath != "" {
|
||||||
|
writeFiles(outputPath, body)
|
||||||
|
} else {
|
||||||
|
writeFiles(fmt.Sprintf("%s.so", pluginName), body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
downloadCmd.PersistentFlags().String("host", "http://localhost:5050", "Set the makeshift remote host (can be set with MAKESHIFT_HOST)")
|
||||||
|
downloadCmd.PersistentFlags().StringP("output", "o", "", "Set the output path to write files")
|
||||||
|
downloadCmd.PersistentFlags().String("cacert", "", "Set the CA certificate path to load")
|
||||||
|
downloadCmd.Flags().StringP("path", "p", ".", "Set the path to list files (can be set with MAKESHIFT_PATH)")
|
||||||
|
downloadCmd.Flags().StringSlice("profiles", []string{}, "Set the profile(s) to use to populate data store")
|
||||||
|
downloadCmd.Flags().StringSlice("plugins", []string{}, "Set the plugin(s) to run before downloading files")
|
||||||
|
downloadCmd.Flags().BoolP("extract", "x", false, "Set whether to extract archive locally after downloading")
|
||||||
|
downloadCmd.Flags().BoolP("remove-archive", "r", false, "Set whether to remove the archive after extracting (used with '--extract' flag)")
|
||||||
|
|
||||||
|
downloadCmd.AddCommand(downloadProfileCmd, downloadPluginCmd)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(&downloadCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to write downloaded files
|
||||||
|
func writeFiles(path string, body []byte) {
|
||||||
|
var err = os.WriteFile(path, body, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to write file(s) from download")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
77
cmd/fetch.go
77
cmd/fetch.go
|
|
@ -1,77 +0,0 @@
|
||||||
//go:build client || all
|
|
||||||
// +build client all
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var fetchCmd = &cobra.Command{
|
|
||||||
Use: "fetch",
|
|
||||||
Short: "Fetch a config file from a remote instance of configurator",
|
|
||||||
Long: "This command is simplified to make a HTTP request to the a configurator service.",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
// make sure a host is set
|
|
||||||
if remoteHost == "" {
|
|
||||||
log.Error().Msg("no '--host' argument set")
|
|
||||||
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 == "" {
|
|
||||||
// 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
|
|
||||||
} 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...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the "Authorization" header if an access token is supplied
|
|
||||||
headers := map[string]string{}
|
|
||||||
if accessToken != "" {
|
|
||||||
headers["Authorization"] = "Bearer " + accessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, target := range targets {
|
|
||||||
// make a request for each target
|
|
||||||
url := fmt.Sprintf("%s/generate?target=%s", remoteHost, target)
|
|
||||||
res, body, err := util.MakeRequest(url, http.MethodGet, nil, headers)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to fetch files")
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
fetchCmd.Flags().StringVar(&remoteHost, "host", "", "set the remote configurator host and 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")
|
|
||||||
|
|
||||||
rootCmd.AddCommand(fetchCmd)
|
|
||||||
}
|
|
||||||
212
cmd/generate.go
212
cmd/generate.go
|
|
@ -1,212 +0,0 @@
|
||||||
//go:build client || all
|
|
||||||
// +build client all
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
|
||||||
"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
|
|
||||||
)
|
|
||||||
|
|
||||||
var generateCmd = &cobra.Command{
|
|
||||||
Use: "generate",
|
|
||||||
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 == "" {
|
|
||||||
// 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
|
|
||||||
} 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// use cert path from cobra if empty
|
|
||||||
if conf.CertPath == "" {
|
|
||||||
conf.CertPath = cacertPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// show conf as JSON and generators if verbose
|
|
||||||
if verbose {
|
|
||||||
b, err := json.MarshalIndent(conf, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to marshal config")
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate files by supplying a list of targets as string values. Currently,
|
|
||||||
// targets are defined statically in a config file. Targets are ran recursively
|
|
||||||
// if more targets are nested in a defined target, but will not run additional
|
|
||||||
// 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) {
|
|
||||||
// generate config with each supplied target
|
|
||||||
for _, target := range targets {
|
|
||||||
outputBytes, err := generator.GenerateWithTarget(conf, target)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Str("target", target).Msg("failed to generate config")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
|
|
||||||
// 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
|
|
||||||
})
|
|
||||||
|
|
||||||
// ...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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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().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)
|
|
||||||
}
|
|
||||||
42
cmd/init.go
Normal file
42
cmd/init.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/service"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var initCmd = &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Example: `
|
||||||
|
# create default files and directories at specified root path
|
||||||
|
# (must be set with positional argument)
|
||||||
|
makeshift init $MAKESHIFT_ROOT
|
||||||
|
`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Initialize directory with default files",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
server *service.Service
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// create the server root files and directories
|
||||||
|
server = service.New()
|
||||||
|
server.RootPath = args[0]
|
||||||
|
err = server.Init()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("root", server.RootPath).
|
||||||
|
Msg("failed to initialize server root")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debug().
|
||||||
|
Str("root", server.RootPath).
|
||||||
|
Msg("initialize makeshift files at root path")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(initCmd)
|
||||||
|
}
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"path/filepath"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
byTarget bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var inspectCmd = &cobra.Command{
|
|
||||||
Use: "inspect",
|
|
||||||
Short: "Inspect generator plugin information",
|
|
||||||
Long: "The 'inspect' sub-command takes a list of directories and prints all found plugin information.",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
// set up table formatter
|
|
||||||
table.DefaultHeaderFormatter = func(format string, vals ...interface{}) string {
|
|
||||||
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)
|
|
||||||
|
|
||||||
// load specific plugins from positional args
|
|
||||||
var generators = make(map[string]generator.Generator)
|
|
||||||
for _, path := range paths {
|
|
||||||
err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
gen, err := generator.LoadPlugin(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
generators[gen.GetName()] = gen
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to walk directory")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// print all generator plugin information found
|
|
||||||
tbl := table.New("Name", "Version", "Description")
|
|
||||||
for _, g := range generators {
|
|
||||||
tbl.AddRow(g.GetName(), g.GetVersion(), g.GetDescription())
|
|
||||||
}
|
|
||||||
if len(generators) > 0 {
|
|
||||||
tbl.Print()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
inspectCmd.Flags().BoolVar(&byTarget, "by-target", false, "set whether to ")
|
|
||||||
rootCmd.AddCommand(inspectCmd)
|
|
||||||
}
|
|
||||||
216
cmd/list.go
Normal file
216
cmd/list.go
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/client"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var listCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Example: `
|
||||||
|
# list files in a remote data directory
|
||||||
|
configurator list --path test
|
||||||
|
configurator list --host http://localhost:5050 --path test
|
||||||
|
|
||||||
|
# list files using 'curl'
|
||||||
|
curl http://localhost:5050/list/test
|
||||||
|
`,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Short: "List all files in a remote data directory",
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
setenv(cmd, "host", "MAKESHIFT_HOST")
|
||||||
|
setenv(cmd, "path", "MAKESHIFT_PATH")
|
||||||
|
setenv(cmd, "cacert", "MAKESHIFT_CACERT")
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
path, _ = cmd.Flags().GetString("path")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
body []byte
|
||||||
|
output []string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", path).
|
||||||
|
Str("cacert", cacertPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make request to /list endpoint
|
||||||
|
_, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: fmt.Sprintf("/list/%s", path),
|
||||||
|
Method: http.MethodGet,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to make request")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &output)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to unmarshal response body")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// show the list of files and directories
|
||||||
|
log.Info().Strs("output", output).Send()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var listPluginsCmd = &cobra.Command{
|
||||||
|
Use: "plugins",
|
||||||
|
Example: `
|
||||||
|
# show all plugins
|
||||||
|
makeshift list plugins
|
||||||
|
|
||||||
|
# show details for specific plugins
|
||||||
|
makeshift list plugins smd jinja2
|
||||||
|
`,
|
||||||
|
Short: "Show plugins information",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
plugins []string
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("cacert", cacertPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
// make request to /list endpoint
|
||||||
|
res, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: "/plugins",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, "/plugins", err)
|
||||||
|
err = json.Unmarshal(body, &plugins)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Msg("failed to unmarshal plugins")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, pluginName := range args {
|
||||||
|
// make request to /list endpoint
|
||||||
|
query = fmt.Sprintf("/plugins/%s/info", pluginName)
|
||||||
|
res, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, query, err)
|
||||||
|
|
||||||
|
plugins = append(plugins, string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info().Strs("plugins", plugins).Send()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var listProfilesCmd = &cobra.Command{
|
||||||
|
Use: "profiles",
|
||||||
|
Example: `
|
||||||
|
# list all profiles
|
||||||
|
makeshift list profiles
|
||||||
|
|
||||||
|
# live individual profiles
|
||||||
|
makeshift list profiles default custom
|
||||||
|
`,
|
||||||
|
Short: "Show all available profiles",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
profiles []makeshift.Profile
|
||||||
|
body []byte
|
||||||
|
query string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("cacert", cacertPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
// make request to /list endpoint
|
||||||
|
res, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: "/profiles",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, "/profiles", err)
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &profiles)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Msg("failed to unmarshal plugins")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, profileID := range args {
|
||||||
|
// make request to /list endpoint
|
||||||
|
query = fmt.Sprintf("/profiles/%s", profileID)
|
||||||
|
res, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, query, err)
|
||||||
|
var profile makeshift.Profile
|
||||||
|
err = json.Unmarshal(body, &profile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Msg("failed to unmarshal plugin")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
profiles = append(profiles, profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info().Any("plugins", profiles).Send()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
listCmd.PersistentFlags().String("host", "http://localhost:5050", "Set the configurator remote host (can be set with MAKESHIFT_HOST)")
|
||||||
|
listCmd.PersistentFlags().String("cacert", "", "Set the CA certificate path to load")
|
||||||
|
listCmd.Flags().StringP("path", "p", ".", "Set the path to list files (can be set with MAKESHIFT_PATH)")
|
||||||
|
|
||||||
|
listCmd.AddCommand(listPluginsCmd, listProfilesCmd)
|
||||||
|
rootCmd.AddCommand(listCmd)
|
||||||
|
}
|
||||||
246
cmd/plugins.go
Normal file
246
cmd/plugins.go
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/client"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/service"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginsCmd = &cobra.Command{
|
||||||
|
Use: "plugins",
|
||||||
|
Short: "Manage, inspect, and compile plugins (requires Go build tools)",
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
setenv(cmd, "host", "MAKESHIFT_HOST")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginsCompileCmd = &cobra.Command{
|
||||||
|
Use: "compile",
|
||||||
|
Example: `
|
||||||
|
# compile plugin using Go build tools
|
||||||
|
go build -buildmode=plugin -o lib/myplugin.so src/plugins/myplugin.go
|
||||||
|
|
||||||
|
# try to compile all plugins in current directory
|
||||||
|
cd src/plugins
|
||||||
|
makeshift plugin compile
|
||||||
|
|
||||||
|
# try to compile all plugins in specified directory
|
||||||
|
makeshift plugin compile src/plugins
|
||||||
|
|
||||||
|
# compile 'src/plugins/myplugin.go' and save to 'lib/myplugin.so'
|
||||||
|
makeshift plugin compile src/plugins/myplugin.go -o lib/myplugin.so
|
||||||
|
`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
outputPath, _ = cmd.Flags().GetString("output")
|
||||||
|
output []byte
|
||||||
|
fileInfo os.FileInfo
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// make the directory
|
||||||
|
err = os.MkdirAll(filepath.Dir(outputPath), 0o777)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed to make output directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// one arg passed, so determine if it is file or directory
|
||||||
|
if len(args) > 0 {
|
||||||
|
if fileInfo, err = os.Stat(args[0]); err == nil {
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
err = compilePluginsDir(args[0], outputPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).
|
||||||
|
Bytes("output", output).
|
||||||
|
Msg("failed to compile plugin")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// not a directory so check if Go file so try and compile it
|
||||||
|
if filepath.Ext(args[0]) == ".go" {
|
||||||
|
output, err = compilePlugin(outputPath, args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).
|
||||||
|
Bytes("output", output).
|
||||||
|
Msg("failed to compile plugin")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Fatal().Msg("argument is not a valid plugin (must be Go file)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
log.Fatal().Err(err).Msgf("failed to stat provided plugin path")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no args passed, so use current directory
|
||||||
|
err = compilePluginsDir(".", outputPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).
|
||||||
|
Bytes("output", output).
|
||||||
|
Msg("failed to compile plugin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginsInspectCmd = &cobra.Command{
|
||||||
|
Use: "inspect",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Example: `
|
||||||
|
# inspect a plugin and print its information
|
||||||
|
makeshift plugin inspect lib/jinja2.so
|
||||||
|
`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
for _, path := range args {
|
||||||
|
var (
|
||||||
|
plugin makeshift.Plugin
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
plugin, err = service.LoadPluginFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to load plugin from file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Any("plugin", map[string]any{
|
||||||
|
"name": plugin.Name(),
|
||||||
|
"version": plugin.Version(),
|
||||||
|
"description": plugin.Description(),
|
||||||
|
"metadata": plugin.Metadata(),
|
||||||
|
}).Send()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginsInfoCmd = &cobra.Command{
|
||||||
|
Use: "info",
|
||||||
|
Example: `
|
||||||
|
# show information of a remote plugin
|
||||||
|
makeshift plugins info jinja2 smd
|
||||||
|
|
||||||
|
# show information of a local plugin (same as 'makeshift inspect')
|
||||||
|
makeshift plugins info --local $MAKESHIFT_ROOT/plugins/jinja2.so
|
||||||
|
`,
|
||||||
|
Short: "Show plugin information",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
outputPath, _ = cmd.Flags().GetString("output")
|
||||||
|
local, _ = cmd.Flags().GetBool("local")
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("output", outputPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if local {
|
||||||
|
var (
|
||||||
|
plugins []map[string]any
|
||||||
|
plugin makeshift.Plugin
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for _, path := range args {
|
||||||
|
plugin, err = service.LoadPluginFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to load plugin from path")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
plugins = append(plugins, makeshift.PluginToMap(plugin))
|
||||||
|
}
|
||||||
|
log.Info().Any("plugins", plugins).Send()
|
||||||
|
} else {
|
||||||
|
for _, pluginName := range args {
|
||||||
|
query = fmt.Sprintf("/plugins/%s/info", pluginName)
|
||||||
|
res, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Str("query", query).
|
||||||
|
Msg("failed to make request")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
log.Error().
|
||||||
|
Any("status", map[string]any{
|
||||||
|
"code": res.StatusCode,
|
||||||
|
"message": res.Status,
|
||||||
|
"body": string(body),
|
||||||
|
}).
|
||||||
|
Str("host", host).
|
||||||
|
Msg("response returned bad status")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if outputPath != "" {
|
||||||
|
writeFiles(outputPath, body)
|
||||||
|
} else {
|
||||||
|
fmt.Println(string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginsCompileCmd.Flags().StringP("output", "o", "", "Set the path to save compiled plugin (matches source type, i.e. uses files or directory)")
|
||||||
|
pluginsInfoCmd.Flags().String("host", "http://localhost:5050", "Set the makeshift remote host (can be set with MAKESHIFT_HOST)")
|
||||||
|
pluginsInfoCmd.Flags().Bool("local", false, "Set whether to display information of a local plugin")
|
||||||
|
|
||||||
|
pluginsCmd.AddCommand(pluginsCompileCmd, pluginsInspectCmd, pluginsInfoCmd)
|
||||||
|
rootCmd.AddCommand(pluginsCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compilePlugin(outputPath string, srcPath string) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
commandArgs string
|
||||||
|
command *exec.Cmd
|
||||||
|
)
|
||||||
|
// execute command to build the plugin
|
||||||
|
commandArgs = fmt.Sprintf("go build -buildmode=plugin -o=%s %s", outputPath, srcPath)
|
||||||
|
command = exec.Command("bash", "-c", commandArgs)
|
||||||
|
return command.CombinedOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
func compilePluginsDir(dirpath string, outputPath string) error {
|
||||||
|
err := filepath.WalkDir(dirpath, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
// not a directory and is Go file, so try and compile it
|
||||||
|
if !d.IsDir() && filepath.Ext(path) == ".go" {
|
||||||
|
var (
|
||||||
|
localOutputPath string = outputPath + "/" + path
|
||||||
|
)
|
||||||
|
output, err := compilePlugin(localOutputPath, path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).
|
||||||
|
Bytes("output", output).
|
||||||
|
Str("path", localOutputPath).
|
||||||
|
Msg("failed to compile plugin")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
124
cmd/root.go
124
cmd/root.go
|
|
@ -2,37 +2,58 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
logger "git.towk2.me/towk/makeshift/pkg/log"
|
||||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
conf config.Config
|
loglevel logger.LogLevel = logger.INFO
|
||||||
configPath string
|
|
||||||
cacertPath string
|
|
||||||
verbose bool
|
|
||||||
targets []string
|
|
||||||
outputPath string
|
|
||||||
accessToken string
|
|
||||||
remoteHost string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = cobra.Command{
|
||||||
Use: "configurator",
|
Use: "makeshift",
|
||||||
Short: "Dynamically generate files defined by generators",
|
Short: "Extensible file cobbler",
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
logFile string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// initialize the logger
|
||||||
|
logFile, _ = cmd.Flags().GetString("log-file")
|
||||||
|
err = logger.InitWithLogLevel(loglevel, logFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to initialize logger")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// try and set flags using env vars
|
||||||
|
setenv(cmd, "log-file", "MAKESHIFT_LOG_FILE")
|
||||||
|
setenv(cmd, "log-level", "MAKESHIFT_LOG_LEVEL")
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
cmd.Help()
|
err := cmd.Help()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to print help")
|
||||||
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
PostRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
log.Debug().Msg("closing log file")
|
||||||
|
err := logger.LogFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to close log file")
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
// run the main program
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -40,39 +61,60 @@ func Execute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(InitConfig)
|
cobra.OnInitialize(
|
||||||
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config path")
|
initLogger,
|
||||||
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)")
|
// initialize the config a single time
|
||||||
|
rootCmd.PersistentFlags().VarP(&loglevel, "log-level", "l", "Set the log level output")
|
||||||
|
rootCmd.PersistentFlags().String("log-file", "", "Set the log file path (can be set with MAKESHIFT_LOG_FILE)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitConfig() {
|
func setenv(cmd *cobra.Command, varname string, envvar string) {
|
||||||
// empty from not being set
|
if cmd.Flags().Changed(varname) {
|
||||||
if configPath != "" {
|
return
|
||||||
exists, err := util.PathExists(configPath)
|
}
|
||||||
|
val := os.Getenv(envvar)
|
||||||
|
if val != "" {
|
||||||
|
cmd.Flags().Set(varname, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setenvp(cmd *cobra.Command, varname string, envvar string) {
|
||||||
|
if cmd.Flags().Changed(varname) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val := os.Getenv(envvar)
|
||||||
|
if val != "" {
|
||||||
|
cmd.PersistentFlags().Set(varname, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initLogger() {
|
||||||
|
// initialize the logger
|
||||||
|
logfile, _ := rootCmd.PersistentFlags().GetString("log-file")
|
||||||
|
err := logger.InitWithLogLevel(loglevel, logfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Str("path", configPath).Msg("failed to load config")
|
log.Error().Err(err).Msg("failed to initialize logger")
|
||||||
os.Exit(1)
|
|
||||||
} else if exists {
|
|
||||||
conf = config.Load(configPath)
|
|
||||||
} else {
|
|
||||||
// show error and exit since a path was specified
|
|
||||||
log.Error().Str("path", configPath).Msg("config file not found")
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// set to the default value and create a new one
|
|
||||||
configPath = "./config.yaml"
|
|
||||||
conf = config.New()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
func handleResponseError(res *http.Response, host, query string, err error) {
|
||||||
// set environment variables to override config values
|
if err != nil {
|
||||||
//
|
log.Error().Err(err).
|
||||||
|
Str("host", host).
|
||||||
// set the JWKS url if we find the CONFIGURATOR_JWKS_URL environment variable
|
Str("query", query).
|
||||||
jwksUrl := os.Getenv("CONFIGURATOR_JWKS_URL")
|
Msg("failed to make request")
|
||||||
if jwksUrl != "" {
|
os.Exit(1)
|
||||||
conf.Server.Jwks.Uri = jwksUrl
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
log.Error().
|
||||||
|
Any("status", map[string]any{
|
||||||
|
"code": res.StatusCode,
|
||||||
|
"message": res.Status,
|
||||||
|
}).
|
||||||
|
Str("host", host).
|
||||||
|
Msg("response returned bad status")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
cmd/run.go
Normal file
28
cmd/run.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
var runCmd = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Example: `
|
||||||
|
NOTE: This command is not implemented yet!
|
||||||
|
|
||||||
|
# set up environment
|
||||||
|
export MAKESHIFT_HOST=http://localhost:5050
|
||||||
|
export MAKESHIFT_PATH=help.txt
|
||||||
|
export MAKESHIFT_ROOT=/opt/makeshift
|
||||||
|
|
||||||
|
# run locally similar to 'download'
|
||||||
|
makeshift run --plugins jinja2 --profiles default
|
||||||
|
makeshift run --root $HOME/apps/makeshift -p help.txt --plugins jinja2 --profiles default
|
||||||
|
`,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Short: "Run locally with plugins and profiles",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(runCmd)
|
||||||
|
}
|
||||||
126
cmd/serve.go
126
cmd/serve.go
|
|
@ -1,67 +1,101 @@
|
||||||
//go:build server || all
|
|
||||||
// +build server all
|
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"net/url"
|
||||||
"errors"
|
"time"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/server"
|
"git.towk2.me/towk/makeshift/pkg/service"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var serveCmd = &cobra.Command{
|
var serveCmd = &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "Start configurator as a server and listen for requests",
|
Example: `
|
||||||
|
# start the service in current directory
|
||||||
|
makeshift serve
|
||||||
|
|
||||||
|
# start the service with root path and initialize
|
||||||
|
makeshift serve --root ./test --init -l debug
|
||||||
|
`,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
setenv(cmd, "host", "MAKESHIFT_HOST")
|
||||||
|
setenv(cmd, "root", "MAKESHIFT_ROOT")
|
||||||
|
setenv(cmd, "timeout", "MAKESHIFT_TIMEOUT")
|
||||||
|
setenv(cmd, "cacert", "MAKESHIFT_CACERT")
|
||||||
|
setenv(cmd, "keyfile", "MAKESHIFT_KEYFILE")
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
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
|
|
||||||
accessToken := os.Getenv("ACCESS_TOKEN")
|
|
||||||
if accessToken != "" {
|
|
||||||
conf.AccessToken = accessToken
|
|
||||||
} else {
|
|
||||||
if verbose {
|
|
||||||
log.Warn().Msg("No token found. Continuing without one...\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// show config as JSON and generators if verbose
|
|
||||||
if verbose {
|
|
||||||
b, err := json.MarshalIndent(conf, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to marshal config")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Printf("%v\n", string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
// start listening with the server
|
|
||||||
var (
|
var (
|
||||||
s *server.Server = server.New(&conf)
|
host, _ = cmd.Flags().GetString("host")
|
||||||
err error = s.Serve()
|
rootPath, _ = cmd.Flags().GetString("root")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
keyfile, _ = cmd.Flags().GetString("keyfile")
|
||||||
|
timeout, _ = cmd.Flags().GetInt("timeout")
|
||||||
|
|
||||||
|
parsed *url.URL
|
||||||
|
server *service.Service
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
if errors.Is(err, http.ErrServerClosed) {
|
|
||||||
if verbose {
|
// parse the host to remove scheme if needed
|
||||||
log.Info().Msg("server closed")
|
parsed, err = url.Parse(host)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Msg("could not parse host")
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to start server")
|
// set the server values
|
||||||
os.Exit(1)
|
server = service.New()
|
||||||
|
server.Addr = parsed.Host
|
||||||
|
server.RootPath = rootPath
|
||||||
|
server.CACertFile = cacertPath
|
||||||
|
server.CACertKeyfile = keyfile
|
||||||
|
server.Timeout = time.Duration(timeout) * time.Second
|
||||||
|
|
||||||
|
// show some debugging information
|
||||||
|
log.Debug().
|
||||||
|
Str("host", parsed.Host).
|
||||||
|
Any("paths", map[string]string{
|
||||||
|
"root": rootPath,
|
||||||
|
"cacert": cacertPath,
|
||||||
|
"keyfile": keyfile,
|
||||||
|
"data": server.PathForData(),
|
||||||
|
"profiles": server.PathForProfiles(),
|
||||||
|
"plugins": server.PathForPlugins(),
|
||||||
|
"metadata": server.PathForMetadata(),
|
||||||
|
}).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
// make the default directories and files if flag is passed
|
||||||
|
if cmd.Flags().Changed("init") {
|
||||||
|
err = server.Init()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("host", parsed.Host).
|
||||||
|
Str("root", rootPath).
|
||||||
|
Msg("failed to initialize server root")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve and log why the server closed
|
||||||
|
err = server.Serve()
|
||||||
|
log.Error().Err(err).Msg("server closed")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
serveCmd.Flags().StringVar(&conf.Server.Host, "host", conf.Server.Host, "set the server host and port")
|
serveCmd.Flags().Bool("init", false, "Initializes default files at specified with the '--root' flag")
|
||||||
// serveCmd.Flags().StringVar(&pluginPath, "plugin", "", "set the generator plugins directory path")
|
serveCmd.Flags().String("host", "localhost:5050", "Set the configurator server host (can be set with MAKESHIFT_HOST)")
|
||||||
serveCmd.Flags().StringVar(&conf.Server.Jwks.Uri, "jwks-uri", conf.Server.Jwks.Uri, "set the JWKS url to fetch public key")
|
serveCmd.Flags().String("root", "./", "Set the root path to serve files (can be set with MAKESHIFT_ROOT)")
|
||||||
serveCmd.Flags().IntVar(&conf.Server.Jwks.Retries, "jwks-fetch-retries", conf.Server.Jwks.Retries, "set the JWKS fetch retry count")
|
serveCmd.Flags().IntP("timeout", "t", 60, "Set the timeout in seconds for requests (can be set with MAKESHIFT_TIMEOUT)")
|
||||||
|
serveCmd.Flags().String("cacert", "", "Set the CA certificate path to load (can be set with MAKESHIFT_CACERT)")
|
||||||
|
serveCmd.Flags().String("keyfile", "", "Set the CA key file to use (can be set with MAKESHIFT_KEYFILE)")
|
||||||
|
|
||||||
|
serveCmd.MarkFlagsRequiredTogether("cacert", "keyfile")
|
||||||
|
|
||||||
rootCmd.AddCommand(serveCmd)
|
rootCmd.AddCommand(serveCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
462
cmd/upload.go
Normal file
462
cmd/upload.go
Normal file
|
|
@ -0,0 +1,462 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"maps"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.towk2.me/towk/makeshift/internal/format"
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/client"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/service"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
inputFormat format.DataFormat = format.JSON
|
||||||
|
)
|
||||||
|
|
||||||
|
var uploadCmd = &cobra.Command{
|
||||||
|
Use: "upload",
|
||||||
|
Example: `
|
||||||
|
# upload a single file in root directory
|
||||||
|
makeshift upload -d @compute-base.yaml
|
||||||
|
|
||||||
|
# upload a directory
|
||||||
|
makeshift upload -d @setup/
|
||||||
|
|
||||||
|
# upload an archive (extracted and saved on server)
|
||||||
|
makeshift upload -d @setup.tar.gz -t archive
|
||||||
|
|
||||||
|
# upload multiple files with a specific path (used to set remote location)
|
||||||
|
makeshift upload -d @kubernetes.json -p nodes/kubernetes.json
|
||||||
|
makeshift upload -d @slurm.json -d @compute.json -p nodes
|
||||||
|
`,
|
||||||
|
Short: "Upload files and directories",
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
setenv(cmd, "host", "MAKESHIFT_HOST")
|
||||||
|
setenv(cmd, "path", "MAKESHIFT_PATH")
|
||||||
|
setenv(cmd, "cacert", "MAKESHIFT_CACERT")
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
path, _ = cmd.Flags().GetString("path")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
dataArgs, _ = cmd.Flags().GetStringArray("data")
|
||||||
|
|
||||||
|
inputData = processFiles(dataArgs)
|
||||||
|
useDirectoryPath = len(inputData) > 1
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", path).
|
||||||
|
Str("query", query).
|
||||||
|
Str("cacert", cacertPath).
|
||||||
|
Any("input", inputData).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
for inputPath, contents := range inputData {
|
||||||
|
log.Debug().Str("path", path).Int("size", len(contents)).Send()
|
||||||
|
if useDirectoryPath {
|
||||||
|
query = path + "/" + filepath.Clean(inputPath)
|
||||||
|
} else {
|
||||||
|
// use flag value if supplied
|
||||||
|
if cmd.Flags().Changed("path") {
|
||||||
|
query = path
|
||||||
|
} else {
|
||||||
|
query = inputPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query = fmt.Sprintf("/upload/%s", query)
|
||||||
|
res, _, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Body: contents,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, query, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadProfilesCmd = &cobra.Command{
|
||||||
|
Use: "profile [profile_id]",
|
||||||
|
Example: `
|
||||||
|
# upload a new profile
|
||||||
|
makeshift upload profile -d @compute.json kubernetes.json
|
||||||
|
|
||||||
|
# upload a new profile with a specific path
|
||||||
|
makeshift upload profile -d @kubernetes.json
|
||||||
|
makeshift upload profile -d '{"id": "custom", "data": {}}' kubernetes.json
|
||||||
|
`,
|
||||||
|
Short: "Upload a new profile",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
dataArgs, _ = cmd.Flags().GetStringArray("data")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
profiles = processProfiles(dataArgs)
|
||||||
|
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("query", query).
|
||||||
|
Str("cacert", cacertPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load files from args
|
||||||
|
for i, path := range args {
|
||||||
|
body, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Int("index", i).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to read profile file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var profile *makeshift.Profile
|
||||||
|
err = json.Unmarshal(body, &profile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Int("index", i).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to unmarshal profile")
|
||||||
|
}
|
||||||
|
profiles = append(profiles, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send each loaded profile to server
|
||||||
|
for _, profile := range profiles {
|
||||||
|
if profile == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err = json.Marshal(profile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to marshal profile")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
query = fmt.Sprintf("/profiles/%s", profile.ID)
|
||||||
|
res, body, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, query, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadPluginsCmd = &cobra.Command{
|
||||||
|
Use: "plugin [plugin_name]",
|
||||||
|
Example: `
|
||||||
|
# upload a new plugin
|
||||||
|
makeshift upload plugin -d @slurm.so
|
||||||
|
makeshift upload plugin slurm.so
|
||||||
|
`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Upload a new plugin",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// make one request be host positional argument (restricted to 1 for now)
|
||||||
|
// temp := append(handleArgs(args), processDataArgs(dataArgs)...)
|
||||||
|
var (
|
||||||
|
host, _ = cmd.Flags().GetString("host")
|
||||||
|
dataArgs, _ = cmd.Flags().GetStringArray("data")
|
||||||
|
cacertPath, _ = cmd.Flags().GetString("cacert")
|
||||||
|
|
||||||
|
plugins = processFiles(dataArgs)
|
||||||
|
c = client.New(host)
|
||||||
|
res *http.Response
|
||||||
|
query string
|
||||||
|
body []byte
|
||||||
|
plugin makeshift.Plugin
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Str("query", query).
|
||||||
|
Str("cacert", cacertPath).
|
||||||
|
Send()
|
||||||
|
|
||||||
|
if cacertPath != "" {
|
||||||
|
c.LoadCertificateFromPath(cacertPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load files from args
|
||||||
|
for i, path := range args {
|
||||||
|
body, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Int("index", i).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to read plugin file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins[path] = body
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, contents := range plugins {
|
||||||
|
plugin, err = service.LoadPluginFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to load plugin from file")
|
||||||
|
}
|
||||||
|
|
||||||
|
query = fmt.Sprintf("/plugins/%s", plugin.Name())
|
||||||
|
res, _, err = c.MakeRequest(client.HTTPEnvelope{
|
||||||
|
Path: query,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Body: contents,
|
||||||
|
})
|
||||||
|
handleResponseError(res, host, query, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
uploadCmd.PersistentFlags().String("host", "http://localhost:5050", "Set the makeshift remote host (can be set with MAKESHIFT_HOST)")
|
||||||
|
uploadCmd.PersistentFlags().StringArrayP("data", "d", []string{}, "Set the data to send to specified host (prepend @ for files)")
|
||||||
|
uploadCmd.PersistentFlags().String("cacert", "", "Set the CA certificate path to load")
|
||||||
|
|
||||||
|
uploadCmd.Flags().StringP("path", "p", ".", "Set the path to list files (can be set with MAKESHIFT_PATH)")
|
||||||
|
|
||||||
|
uploadProfilesCmd.Flags().VarP(&inputFormat, "format", "F", "Set the input format for profile")
|
||||||
|
|
||||||
|
uploadCmd.AddCommand(uploadProfilesCmd, uploadPluginsCmd)
|
||||||
|
rootCmd.AddCommand(uploadCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processFiles(args []string) map[string][]byte {
|
||||||
|
// load data either from file or directly from args
|
||||||
|
var collection = make(map[string][]byte, len(args))
|
||||||
|
for _, arg := range args {
|
||||||
|
// skip empty string args
|
||||||
|
if len(arg) > 0 {
|
||||||
|
// determine if we're reading from file to load contents
|
||||||
|
if strings.HasPrefix(arg, "@") {
|
||||||
|
var path string = strings.TrimLeft(arg, "@")
|
||||||
|
|
||||||
|
// process sub-directories recursively
|
||||||
|
newCollection, err := processDir(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().
|
||||||
|
Err(err).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to process directory at path")
|
||||||
|
}
|
||||||
|
log.Trace().
|
||||||
|
Str("path", path).
|
||||||
|
Msg("new collection added at path")
|
||||||
|
maps.Copy(collection, newCollection)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.Warn().Msg("only files can be uploaded (add @ before the path with '--data' flag)")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
// processProfiles takes a slice of strings that check for the @ symbol and loads
|
||||||
|
// the contents from the file specified in place (which replaces the path).
|
||||||
|
//
|
||||||
|
// NOTE: The purpose is to make the input arguments uniform for our request. This
|
||||||
|
// function is meant to handle data passed with the `-d/--data` flag and positional
|
||||||
|
// args from the CLI.
|
||||||
|
func processProfiles(args []string) []*makeshift.Profile {
|
||||||
|
// load data either from file or directly from args
|
||||||
|
var collection = make([]*makeshift.Profile, len(args))
|
||||||
|
for i, arg := range args {
|
||||||
|
// if arg is empty string, then skip and continue
|
||||||
|
if len(arg) > 0 {
|
||||||
|
// determine if we're reading from file to load contents
|
||||||
|
if strings.HasPrefix(arg, "@") {
|
||||||
|
var (
|
||||||
|
path string = strings.TrimLeft(arg, "@")
|
||||||
|
contents []byte
|
||||||
|
data *makeshift.Profile
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
contents, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to read file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip empty files
|
||||||
|
if len(contents) == 0 {
|
||||||
|
log.Warn().
|
||||||
|
Str("path", path).
|
||||||
|
Msg("file is empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert/validate input data
|
||||||
|
data, err = parseProfile(contents, format.DataFormatFromFileExt(path, inputFormat))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("path", path).
|
||||||
|
Msg("failed to validate input from file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add loaded data to collection of all data
|
||||||
|
collection = append(collection, data)
|
||||||
|
} else {
|
||||||
|
// input should be a valid JSON
|
||||||
|
var (
|
||||||
|
data *makeshift.Profile
|
||||||
|
input = []byte(arg)
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if !json.Valid(input) {
|
||||||
|
log.Error().Msgf("argument %d not a valid JSON", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(input, &data)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Msgf("failed to unmarshal input for argument %d", i)
|
||||||
|
}
|
||||||
|
return []*makeshift.Profile{data}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func processDir(path string) (map[string][]byte, error) {
|
||||||
|
var (
|
||||||
|
collection = map[string][]byte{}
|
||||||
|
fileInfo os.FileInfo
|
||||||
|
contents []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
// determine if path is directory
|
||||||
|
if fileInfo, err = os.Stat(path); err == nil {
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if !d.IsDir() {
|
||||||
|
contents, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("path", path).Msg("failed to read file")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip empty files
|
||||||
|
if len(contents) == 0 {
|
||||||
|
log.Warn().Str("path", path).Msg("file is empty")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("path", path).
|
||||||
|
Msg("file added to collection")
|
||||||
|
|
||||||
|
// add loaded data to collection of all data
|
||||||
|
collection[path] = contents
|
||||||
|
} else {
|
||||||
|
// process sub-directories recursively
|
||||||
|
newCollection, err := processDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process directory at path '%s': %v", path, err)
|
||||||
|
}
|
||||||
|
log.Trace().
|
||||||
|
Str("path", path).
|
||||||
|
Msg("new collection added from nested directory")
|
||||||
|
maps.Copy(collection, newCollection)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
contents, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return collection, fmt.Errorf("failed to read file at path '%s': %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip empty files
|
||||||
|
if len(contents) == 0 {
|
||||||
|
return collection, fmt.Errorf("file is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("path", path).
|
||||||
|
Msg("file added to collection")
|
||||||
|
|
||||||
|
// add loaded data to collection of all data
|
||||||
|
collection[path] = contents
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("failed to stat file: %v", err)
|
||||||
|
}
|
||||||
|
return collection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProfile(contents []byte, dataFormat format.DataFormat) (*makeshift.Profile, error) {
|
||||||
|
var (
|
||||||
|
data *makeshift.Profile
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// convert/validate JSON input format
|
||||||
|
err = format.Unmarshal(contents, &data, dataFormat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal profile: %v", err)
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadStdin reads all of standard input and returns the bytes. If an error
|
||||||
|
// occurs during scanning, it is returned.
|
||||||
|
func ReadStdin() ([]byte, error) {
|
||||||
|
var b []byte
|
||||||
|
input := bufio.NewScanner(os.Stdin)
|
||||||
|
for input.Scan() {
|
||||||
|
b = append(b, input.Bytes()...)
|
||||||
|
b = append(b, byte('\n'))
|
||||||
|
if len(b) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := input.Err(); err != nil {
|
||||||
|
return b, fmt.Errorf("failed to read stdin: %w", err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,23 +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
|
|
||||||
#
|
|
||||||
SERVER keepalive=ON
|
|
||||||
SERVER logdir="/var/log/conman"
|
|
||||||
SERVER logfile="/var/log/conman.log"
|
|
||||||
SERVER loopback=ON
|
|
||||||
SERVER pidfile="/var/run/conman.pid"
|
|
||||||
SERVER resetcmd="/usr/bin/powerman -0 \%N; sleep 5; /usr/bin/powerman -1 \%N"
|
|
||||||
SERVER tcpwrappers=ON
|
|
||||||
#SERVER timestamp=1h
|
|
||||||
|
|
||||||
GLOBAL seropts="115200,8n1"
|
|
||||||
GLOBAL log="/var/log/conman/console.\%N"
|
|
||||||
GLOBAL logopts="sanitize,timestamp"
|
|
||||||
|
|
||||||
{{ consoles }}
|
|
||||||
|
|
@ -1,51 +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
|
|
||||||
#
|
|
||||||
allow booting;
|
|
||||||
allow bootp;
|
|
||||||
ddns-update-style interim;
|
|
||||||
authoritative;
|
|
||||||
|
|
||||||
option space ipxe;
|
|
||||||
|
|
||||||
# Tell iPXE to not wait for ProxyDHCP requests to speed up boot.
|
|
||||||
option ipxe.no-pxedhcp code 176 = unsigned integer 8;
|
|
||||||
option ipxe.no-pxedhcp 1;
|
|
||||||
|
|
||||||
option architecture-type code 93 = unsigned integer 16;
|
|
||||||
|
|
||||||
if exists user-class and option user-class = "iPXE" {
|
|
||||||
filename "http://%{IPADDR}/WW/ipxe/cfg/${mac}";
|
|
||||||
} else {
|
|
||||||
if option architecture-type = 00:0B {
|
|
||||||
filename "/warewulf/ipxe/bin-arm64-efi/snp.efi";
|
|
||||||
} elsif option architecture-type = 00:0A {
|
|
||||||
filename "/warewulf/ipxe/bin-arm32-efi/placeholder.efi";
|
|
||||||
} elsif option architecture-type = 00:09 {
|
|
||||||
filename "/warewulf/ipxe/bin-x86_64-efi/snp.efi";
|
|
||||||
} elsif option architecture-type = 00:07 {
|
|
||||||
filename "/warewulf/ipxe/bin-x86_64-efi/snp.efi";
|
|
||||||
} elsif option architecture-type = 00:06 {
|
|
||||||
filename "/warewulf/ipxe/bin-i386-efi/snp.efi";
|
|
||||||
} elsif option architecture-type = 00:00 {
|
|
||||||
filename "/warewulf/ipxe/bin-i386-pcbios/undionly.kpxe";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subnet %{NETWORK} netmask %{NETMASK} {
|
|
||||||
not authoritative;
|
|
||||||
# option interface-mtu 9000;
|
|
||||||
option subnet-mask %{NETMASK};
|
|
||||||
}
|
|
||||||
|
|
||||||
# Compute Nodes (WIP - see the dhcpd generator plugin)
|
|
||||||
{{ compute_nodes }}
|
|
||||||
|
|
||||||
# Node entries will follow below
|
|
||||||
{{ node_entries }}
|
|
||||||
|
|
@ -1,10 +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
|
|
||||||
#
|
|
||||||
{{ dhcp_hosts }}
|
|
||||||
|
|
@ -1,18 +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
|
|
||||||
#
|
|
||||||
include "/etc/powerman/ipmipower.dev"
|
|
||||||
include "/etc/powerman/ipmi.dev"
|
|
||||||
|
|
||||||
|
|
||||||
# list of devices
|
|
||||||
{{ devices }}
|
|
||||||
|
|
||||||
# create nodes based on found nodes in hostfile
|
|
||||||
{{ nodes }}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
31
go.mod
31
go.mod
|
|
@ -1,20 +1,21 @@
|
||||||
module github.com/OpenCHAMI/configurator
|
module git.towk2.me/towk/makeshift
|
||||||
|
|
||||||
go 1.21.5
|
go 1.24.4
|
||||||
|
|
||||||
|
toolchain go1.24.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/OpenCHAMI/jwtauth/v5 v5.0.0-20240321222802-e6cb468a2a18
|
github.com/OpenCHAMI/jwtauth/v5 v5.0.0-20240321222802-e6cb468a2a18
|
||||||
|
github.com/cavaliergopher/grab/v3 v3.0.1
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
github.com/lestrrat-go/jwx/v2 v2.1.1
|
github.com/lestrrat-go/jwx/v2 v2.1.1
|
||||||
github.com/nikolalohinski/gonja/v2 v2.2.0
|
github.com/nikolalohinski/gonja/v2 v2.3.5
|
||||||
github.com/openchami/chi-middleware/auth v0.0.0-20240812224658-b16b83c70700
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700
|
|
||||||
github.com/rodaine/table v1.2.0
|
|
||||||
github.com/rs/zerolog v1.33.0
|
|
||||||
github.com/sirupsen/logrus v1.9.3
|
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
|
github.com/tidwall/sjson v1.2.5
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
go.yaml.in/yaml/v3 v3.0.4
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
@ -23,7 +24,7 @@ require (
|
||||||
github.com/goccy/go-json v0.10.3 // indirect
|
github.com/goccy/go-json v0.10.3 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kr/text v0.1.0 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||||
|
|
@ -35,8 +36,12 @@ require (
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/tidwall/gjson v1.14.2 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
golang.org/x/crypto v0.25.0 // indirect
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/text v0.16.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
98
go.sum
98
go.sum
|
|
@ -2,8 +2,11 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/OpenCHAMI/jwtauth/v5 v5.0.0-20240321222802-e6cb468a2a18 h1:oBPtXp9RVm9lk5zTmDLf+Vh21yDHpulBxUqGJQjwQCk=
|
github.com/OpenCHAMI/jwtauth/v5 v5.0.0-20240321222802-e6cb468a2a18 h1:oBPtXp9RVm9lk5zTmDLf+Vh21yDHpulBxUqGJQjwQCk=
|
||||||
github.com/OpenCHAMI/jwtauth/v5 v5.0.0-20240321222802-e6cb468a2a18/go.mod h1:ggNHWgLfW/WRXcE8ZZC4S7UwHif16HVmyowOCWdNSN8=
|
github.com/OpenCHAMI/jwtauth/v5 v5.0.0-20240321222802-e6cb468a2a18/go.mod h1:ggNHWgLfW/WRXcE8ZZC4S7UwHif16HVmyowOCWdNSN8=
|
||||||
|
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
|
||||||
|
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|
@ -13,27 +16,26 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
|
|
@ -51,34 +53,27 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/nikolalohinski/gonja/v2 v2.2.0 h1:tAs3BDHNjvPj48F2BL5t7iVhN32HhgeldAl3EmdsLh8=
|
github.com/nikolalohinski/gonja/v2 v2.3.5 h1:7ukCnsokmOIGXOjgW/WrM+xqgwjsQcU0ejFrrz4HQXk=
|
||||||
github.com/nikolalohinski/gonja/v2 v2.2.0/go.mod h1:l9DuWJvT/BddBr2SsmEimESD6msSqRw7u5HzI2Um+sc=
|
github.com/nikolalohinski/gonja/v2 v2.3.5/go.mod h1:UIzXPVuOsr5h7dZ5DUbqk3/Z7oFA/NLGQGMjqT4L2aU=
|
||||||
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
|
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
|
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||||
github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ=
|
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/openchami/chi-middleware/auth v0.0.0-20240812224658-b16b83c70700 h1:XADGipD2FZ9swuFUqeL7h63j3voiq9qA7P0aKsqgZKg=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/openchami/chi-middleware/auth v0.0.0-20240812224658-b16b83c70700/go.mod h1:kswb9kU5cZAFRAvf1dAUJRWbQyjDEb0qkxW4ncDdEXg=
|
|
||||||
github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700 h1:Gzt5f6RK39CHvY3SJudzBb/RK4tVh/S3CpJ0eQlbNdg=
|
|
||||||
github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700/go.mod h1:UuXvr2loD4MtvZeKr57W0WpBs+gm0KM1kdtcXrE8M6s=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
|
@ -89,38 +84,43 @@ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyh
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
220
internal/archive/archive.go
Normal file
220
internal/archive/archive.go
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Create(filenames []string, buf io.Writer, hooks []makeshift.Hook) 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 _, filename := range filenames {
|
||||||
|
err := addToArchive(tw, filename, hooks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Expand(tarname, xpath string) error {
|
||||||
|
tarfile, err := os.Open(tarname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tarfile.Close()
|
||||||
|
// absPath, err := filepath.Abs(xpath)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
tr := tar.NewReader(tarfile)
|
||||||
|
if strings.HasSuffix(tarname, ".gz") {
|
||||||
|
gz, err := gzip.NewReader(tarfile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create new gzip reader: %v", err)
|
||||||
|
}
|
||||||
|
defer gz.Close()
|
||||||
|
tr = tar.NewReader(gz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// untar each segment
|
||||||
|
for {
|
||||||
|
header, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get next tar header: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine proper file path info
|
||||||
|
var (
|
||||||
|
fileinfo = header.FileInfo()
|
||||||
|
filename = header.Name
|
||||||
|
file *os.File
|
||||||
|
abspath string
|
||||||
|
dirpath string
|
||||||
|
)
|
||||||
|
|
||||||
|
// absFileName := filepath.Join(absPath, filename) // if a dir, create it, then go to next segment
|
||||||
|
if fileinfo.Mode().IsDir() {
|
||||||
|
if err := os.MkdirAll(filename, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to make directory '%s': %v", filename, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dirpath = filepath.Dir(filename)
|
||||||
|
if err = os.MkdirAll(dirpath, 0o777); err != nil {
|
||||||
|
return fmt.Errorf("failed to make directory '%s': %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new file with original file mode
|
||||||
|
abspath, err = filepath.Abs(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute path: %v", err)
|
||||||
|
}
|
||||||
|
file, err = os.OpenFile(
|
||||||
|
abspath,
|
||||||
|
os.O_RDWR|os.O_CREATE|os.O_TRUNC,
|
||||||
|
fileinfo.Mode().Perm(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open file: %v", err)
|
||||||
|
}
|
||||||
|
// fmt.Printf("x %s\n", filename)
|
||||||
|
|
||||||
|
// copy the contents to the new file
|
||||||
|
n, err := io.Copy(file, tr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to copy file: %v", err)
|
||||||
|
}
|
||||||
|
if err = file.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close file: %v", err)
|
||||||
|
}
|
||||||
|
if n != fileinfo.Size() {
|
||||||
|
return fmt.Errorf("wrote %d, want %d", n, fileinfo.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addToArchive(tw *tar.Writer, filename string, hooks []makeshift.Hook) error {
|
||||||
|
var (
|
||||||
|
tempfile = fmt.Sprintf("%s.tmp", filename)
|
||||||
|
file *os.File
|
||||||
|
contents []byte
|
||||||
|
data any
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
// run pre-hooks to modify the contents of the file
|
||||||
|
// before archiving using plugins
|
||||||
|
for _, hook := range hooks {
|
||||||
|
// set the file in the data store before running hook
|
||||||
|
contents, err = os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read '%s' to download: %v", filename, err)
|
||||||
|
}
|
||||||
|
hook.Data.Set("file", contents)
|
||||||
|
|
||||||
|
err = hook.Init()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = hook.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = hook.Cleanup()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create temporary file to use to add to archive
|
||||||
|
hook = hooks[len(hooks)-1]
|
||||||
|
data, err = hook.Data.Get("out")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get output data from '%s' plugin: %v", hook.Plugin.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(tempfile, data.([]byte), 0o777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write temporary file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use original file if no hooks to write archive
|
||||||
|
if len(hooks) == 0 {
|
||||||
|
file, err = os.Open(filename)
|
||||||
|
} else {
|
||||||
|
file, err = os.Open(tempfile)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open archive file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// get FileInfo for file size, mode, etc.
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip file if it's a directory
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a tar Header from the FileInfo data
|
||||||
|
header, err := tar.FileInfoHeader(info, info.Name())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create FileInfoHeader: %v", 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
|
||||||
|
if len(hooks) == 0 {
|
||||||
|
_, err = io.Copy(tw, file)
|
||||||
|
} else {
|
||||||
|
_, err = io.Copy(tw, strings.NewReader(string(data.([]byte))))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the temporary file since we're done with it
|
||||||
|
if len(hooks) != 0 {
|
||||||
|
err = os.Remove(tempfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
104
internal/format/format.go
Normal file
104
internal/format/format.go
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
List DataFormat = "list"
|
||||||
|
JSON DataFormat = "json"
|
||||||
|
YAML DataFormat = "yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (df DataFormat) String() string {
|
||||||
|
return string(df)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (df *DataFormat) Set(v string) error {
|
||||||
|
switch DataFormat(v) {
|
||||||
|
case List, JSON, YAML:
|
||||||
|
*df = DataFormat(v)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("must be one of %v", []DataFormat{
|
||||||
|
List, JSON, YAML,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (df DataFormat) Type() string {
|
||||||
|
return "DataFormat"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalData marshals arbitrary data into a byte slice formatted as outFormat.
|
||||||
|
// If a marshalling error occurs or outFormat is unknown, an error is returned.
|
||||||
|
//
|
||||||
|
// Supported values are: json, list, yaml
|
||||||
|
func Marshal(data interface{}, outFormat DataFormat) ([]byte, error) {
|
||||||
|
switch outFormat {
|
||||||
|
case JSON:
|
||||||
|
if bytes, err := json.MarshalIndent(data, "", " "); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal data into JSON: %w", err)
|
||||||
|
} else {
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
case YAML:
|
||||||
|
if bytes, err := yaml.Marshal(data); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal data into YAML: %w", err)
|
||||||
|
} else {
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
case List:
|
||||||
|
return nil, fmt.Errorf("this data format cannot be marshaled")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown data format: %s", outFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalData unmarshals a byte slice formatted as inFormat into an interface
|
||||||
|
// v. If an unmarshalling error occurs or inFormat is unknown, an error is
|
||||||
|
// returned.
|
||||||
|
//
|
||||||
|
// Supported values are: json, list, yaml
|
||||||
|
func Unmarshal(data []byte, v interface{}, inFormat DataFormat) error {
|
||||||
|
switch inFormat {
|
||||||
|
case JSON:
|
||||||
|
if err := json.Unmarshal(data, v); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal data into JSON: %w", err)
|
||||||
|
}
|
||||||
|
case YAML:
|
||||||
|
if err := yaml.Unmarshal(data, v); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal data into YAML: %w", err)
|
||||||
|
}
|
||||||
|
case List:
|
||||||
|
return fmt.Errorf("this data format cannot be unmarshaled")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown data format: %s", inFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataFormatFromFileExt determines the type of the contents
|
||||||
|
// (JSON or YAML) based on the filname extension. The default
|
||||||
|
// format is passed in, so if it doesn't match one of the cases,
|
||||||
|
// that's what we will use. The defaultFmt value takes into account
|
||||||
|
// both the standard default format (JSON) and any command line
|
||||||
|
// change to that provided by options.
|
||||||
|
func DataFormatFromFileExt(path string, defaultFmt DataFormat) DataFormat {
|
||||||
|
switch filepath.Ext(path) {
|
||||||
|
case ".json", ".JSON":
|
||||||
|
// The file is a JSON file
|
||||||
|
return JSON
|
||||||
|
case ".yaml", ".yml", ".YAML", ".YML":
|
||||||
|
// The file is a YAML file
|
||||||
|
return YAML
|
||||||
|
}
|
||||||
|
return defaultFmt
|
||||||
|
}
|
||||||
0
lib/.gitkeep
Normal file
0
lib/.gitkeep
Normal file
2
main.go
2
main.go
|
|
@ -1,6 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/OpenCHAMI/configurator/cmd"
|
import "git.towk2.me/towk/makeshift/cmd"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
|
|
|
||||||
|
|
@ -3,45 +3,139 @@ package client
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/util"
|
||||||
|
"github.com/cavaliergopher/grab/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option func(*Params)
|
type HTTPBody []byte
|
||||||
type Params struct {
|
type HTTPHeader map[string]string
|
||||||
Host string `yaml:"host"`
|
type HTTPEnvelope struct {
|
||||||
AccessToken string `yaml:"access-token"`
|
Path string
|
||||||
Transport *http.Transport
|
Method string
|
||||||
|
Header HTTPHeader
|
||||||
|
Body HTTPBody
|
||||||
|
CACert string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToParams(opts ...Option) *Params {
|
type Client struct {
|
||||||
params := &Params{}
|
http.Client
|
||||||
for _, opt := range opts {
|
BaseURI string
|
||||||
opt(params)
|
AccessToken string
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithHost(host string) Option {
|
func New(uri string) Client {
|
||||||
return func(c *Params) {
|
return Client{
|
||||||
c.Host = host
|
BaseURI: strings.TrimSuffix(uri, "/"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithAccessToken(token string) Option {
|
func NewHTTPEnvelope() HTTPEnvelope {
|
||||||
return func(c *Params) {
|
return HTTPEnvelope{
|
||||||
c.AccessToken = token
|
Path: "",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: nil,
|
||||||
|
Body: nil,
|
||||||
|
CACert: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithCertPool(certPool *x509.CertPool) Option {
|
func (c *Client) MakeRequest(env HTTPEnvelope) (*http.Response, []byte, error) {
|
||||||
return func(c *Params) {
|
return util.MakeRequest(c.BaseURI+env.Path, env.Method, env.Body, env.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Download(out string, env HTTPEnvelope) (*grab.Response, error) {
|
||||||
|
if out == "" {
|
||||||
|
return grab.Get(out, c.BaseURI+env.Path)
|
||||||
|
}
|
||||||
|
return grab.Get(out, c.BaseURI+env.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UploadMultipartFile(uri, key, path string) (*http.Response, error) {
|
||||||
|
body, writer := io.Pipe()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, uri, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mwriter := multipart.NewWriter(writer)
|
||||||
|
req.Header.Add("Content-Type", mwriter.FormDataContentType())
|
||||||
|
|
||||||
|
errchan := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(errchan)
|
||||||
|
defer writer.Close()
|
||||||
|
defer mwriter.Close()
|
||||||
|
|
||||||
|
w, err := mwriter.CreateFormFile(key, path)
|
||||||
|
if err != nil {
|
||||||
|
errchan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
errchan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
if written, err := io.Copy(w, in); err != nil {
|
||||||
|
errchan <- fmt.Errorf("error copying %s (%d bytes written): %v", path, written, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mwriter.Close(); err != nil {
|
||||||
|
errchan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
merr := <-errchan
|
||||||
|
|
||||||
|
if err != nil || merr != nil {
|
||||||
|
return resp, fmt.Errorf("http error: %v, multipart error: %v", err, merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) LoadCertificateFromPath(path string) error {
|
||||||
|
cacert, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read certificate at path: %s", path)
|
||||||
|
}
|
||||||
|
certPool := x509.NewCertPool()
|
||||||
|
certPool.AppendCertsFromPEM(cacert)
|
||||||
|
err = c.LoadCertificateFromPool(certPool)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not initialize certificate from pool: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) LoadCertificateFromPool(certPool *x509.CertPool) error {
|
||||||
|
// make sure we have a valid cert pool
|
||||||
|
if certPool == nil {
|
||||||
|
return fmt.Errorf("invalid cert pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that we can access the internal client
|
||||||
c.Transport = &http.Transport{
|
c.Transport = &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
RootCAs: certPool,
|
RootCAs: certPool,
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: false,
|
||||||
},
|
},
|
||||||
DisableKeepAlives: true,
|
DisableKeepAlives: true,
|
||||||
Dial: (&net.Dialer{
|
Dial: (&net.Dialer{
|
||||||
|
|
@ -51,16 +145,13 @@ func WithCertPool(certPool *x509.CertPool) Option {
|
||||||
TLSHandshakeTimeout: 120 * time.Second,
|
TLSHandshakeTimeout: 120 * time.Second,
|
||||||
ResponseHeaderTimeout: 120 * time.Second,
|
ResponseHeaderTimeout: 120 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Need to check for errors when reading from a file
|
func mustOpen(f string) *os.File {
|
||||||
func WithCertPoolFile(certPath string) Option {
|
r, err := os.Open(f)
|
||||||
if certPath == "" {
|
if err != nil {
|
||||||
return func(sc *Params) {}
|
panic(err)
|
||||||
}
|
}
|
||||||
cacert, _ := os.ReadFile(certPath)
|
return r
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
certPool.AppendCertsFromPEM(cacert)
|
|
||||||
return WithCertPool(certPool)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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:"-"`
|
|
||||||
Host string `yaml:"host"`
|
|
||||||
Port int `yaml:"port"`
|
|
||||||
AccessToken string `yaml:"access-token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constructor function that allows supplying Option 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,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
var (
|
|
||||||
eths = []configurator.EthernetInterface{}
|
|
||||||
bytes []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
// make request to SMD endpoint
|
|
||||||
bytes, 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)
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
return eths, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
var (
|
|
||||||
comps = []configurator.Component{}
|
|
||||||
bytes []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
// make request to SMD endpoint
|
|
||||||
bytes, 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// unmarshal response body JSON and extract in object
|
|
||||||
var tmp map[string]any
|
|
||||||
err = json.Unmarshal(bytes, &tmp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
|
||||||
}
|
|
||||||
bytes, err = json.Marshal(tmp["RedfishEndpoints"].([]any))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal JSON: %v", err)
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(bytes, &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")
|
|
||||||
}
|
|
||||||
|
|
||||||
return comps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: improve implementation of this function
|
|
||||||
func (client *SmdClient) FetchRedfishEndpoints(verbose bool) ([]configurator.RedfishEndpoint, error) {
|
|
||||||
var (
|
|
||||||
eps = []configurator.RedfishEndpoint{}
|
|
||||||
tmp map[string]any
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(b, &eps)
|
|
||||||
if err != nil {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
return eps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) {
|
|
||||||
if client == nil {
|
|
||||||
return nil, fmt.Errorf("client is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch DHCP related information from SMD's endpoint:
|
|
||||||
url := fmt.Sprintf("%s/hsm/v2%s", client.Host, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// include access token in authorzation header if found
|
|
||||||
// NOTE: This shouldn't be needed for this endpoint since it's public
|
|
||||||
if client.AccessToken != "" {
|
|
||||||
req.Header.Add("Authorization", "Bearer "+client.AccessToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make the request to SMD
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to make request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// read the contents of the response body
|
|
||||||
return io.ReadAll(res.Body)
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EthernetInterface struct {
|
|
||||||
Id string
|
|
||||||
Description string
|
|
||||||
MacAddress string
|
|
||||||
LastUpdate string
|
|
||||||
ComponentId string
|
|
||||||
Type string
|
|
||||||
IpAddresses []IPAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
type Component struct {
|
|
||||||
ID string `json:"ID"`
|
|
||||||
Type string `json:"Type"`
|
|
||||||
State string `json:"State,omitempty"`
|
|
||||||
Flag string `json:"Flag,omitempty"`
|
|
||||||
Enabled *bool `json:"Enabled,omitempty"`
|
|
||||||
SwStatus string `json:"SoftwareStatus,omitempty"`
|
|
||||||
Role string `json:"Role,omitempty"`
|
|
||||||
SubRole string `json:"SubRole,omitempty"`
|
|
||||||
NID json.Number `json:"NID,omitempty"`
|
|
||||||
Subtype string `json:"Subtype,omitempty"`
|
|
||||||
NetType string `json:"NetType,omitempty"`
|
|
||||||
Arch string `json:"Arch,omitempty"`
|
|
||||||
Class string `json:"Class,omitempty"`
|
|
||||||
ReservationDisabled bool `json:"ReservationDisabled,omitempty"`
|
|
||||||
Locked bool `json:"Locked,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RedfishEndpoint struct {
|
|
||||||
ID string `json:"ID"`
|
|
||||||
Type string `json:"Type"`
|
|
||||||
Name string `json:"Name,omitempty"` // user supplied descriptive name
|
|
||||||
Hostname string `json:"Hostname"`
|
|
||||||
Domain string `json:"Domain"`
|
|
||||||
FQDN string `json:"FQDN"`
|
|
||||||
Enabled bool `json:"Enabled"`
|
|
||||||
UUID string `json:"UUID,omitempty"`
|
|
||||||
User string `json:"User"`
|
|
||||||
Password string `json:"Password"` // Temporary until more secure method
|
|
||||||
UseSSDP bool `json:"UseSSDP,omitempty"`
|
|
||||||
MACRequired bool `json:"MACRequired,omitempty"`
|
|
||||||
MACAddr string `json:"MACAddr,omitempty"`
|
|
||||||
IPAddr string `json:"IPAddress,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Node struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type BMC struct {
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +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 Conman struct{}
|
|
||||||
|
|
||||||
func (g *Conman) GetName() string {
|
|
||||||
return "conman"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Conman) GetVersion() string {
|
|
||||||
return util.GitCommit()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
var (
|
|
||||||
smdClient = client.NewSmdClient(params.ClientOpts...)
|
|
||||||
eps = []configurator.RedfishEndpoint{}
|
|
||||||
err error = nil
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// format output to write to config file
|
|
||||||
consoles = "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n"
|
|
||||||
for _, ep := range eps {
|
|
||||||
consoles += fmt.Sprintf("CONSOLE name=%s dev=ipmi:%s-bmc ipmiopts=U:%s,P:%s,W:solpayloadsize\n", ep.Name, ep.Name, ep.User, ep.Password)
|
|
||||||
}
|
|
||||||
consoles += "# ====================================================================="
|
|
||||||
|
|
||||||
// apply template substitutions and return output as byte array
|
|
||||||
return ApplyTemplates(Mappings{
|
|
||||||
"plugin_name": g.GetName(),
|
|
||||||
"plugin_version": g.GetVersion(),
|
|
||||||
"plugin_description": g.GetDescription(),
|
|
||||||
"server_opts": "",
|
|
||||||
"global_opts": "",
|
|
||||||
"consoles": consoles,
|
|
||||||
}, params.Templates)
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package generator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CoreDhcp struct{}
|
|
||||||
|
|
||||||
func (g *CoreDhcp) GetName() string {
|
|
||||||
return "coredhcp"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *CoreDhcp) GetVersion() string {
|
|
||||||
return util.GitCommit()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return nil, fmt.Errorf("plugin does not implement generation function")
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +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 DNSMasq struct{}
|
|
||||||
|
|
||||||
func (g *DNSMasq) GetName() string {
|
|
||||||
return "dnsmasq"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *DNSMasq) GetVersion() string {
|
|
||||||
return util.GitCommit()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
// make sure we have a valid config first
|
|
||||||
if config == nil {
|
|
||||||
return nil, fmt.Errorf("invalid config (config is nil)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// set all the defaults for variables
|
|
||||||
var (
|
|
||||||
smdClient = client.NewSmdClient(params.ClientOpts...)
|
|
||||||
eths = []configurator.EthernetInterface{}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
output := "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n"
|
|
||||||
for _, eth := range eths {
|
|
||||||
if eth.Type == "NodeBMC" {
|
|
||||||
output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n"
|
|
||||||
} else {
|
|
||||||
output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output += "# ====================================================================="
|
|
||||||
|
|
||||||
// apply template substitutions and return output as byte array
|
|
||||||
return ApplyTemplates(Mappings{
|
|
||||||
"plugin_name": g.GetName(),
|
|
||||||
"plugin_version": g.GetVersion(),
|
|
||||||
"plugin_description": g.GetDescription(),
|
|
||||||
"dhcp_hosts": output,
|
|
||||||
}, params.Templates)
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package generator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Example struct {
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Example) GetName() string {
|
|
||||||
return "example"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Example) GetVersion() string {
|
|
||||||
return util.GitCommit()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,264 +0,0 @@
|
||||||
package generator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Mappings map[string]any
|
|
||||||
FileMap map[string][]byte
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts the file outputs from map[string][]byte to map[string]string.
|
|
||||||
func ConvertContentsToString(f FileMap) map[string]string {
|
|
||||||
n := make(map[string]string, len(f))
|
|
||||||
for k, v := range f {
|
|
||||||
n[k] = string(v)
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads files without applying any Jinja 2 templating.
|
|
||||||
func LoadFiles(paths ...string) (FileMap, error) {
|
|
||||||
var outputs = FileMap{}
|
|
||||||
for _, path := range paths {
|
|
||||||
expandedPaths, err := filepath.Glob(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to glob path: %w", 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)
|
|
||||||
}
|
|
||||||
// skip any directories found
|
|
||||||
if info.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
b, err := os.ReadFile(expandedPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outputs[expandedPath] = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return outputs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads a single generator plugin given a single file path.
|
|
||||||
func LoadPlugin(path string) (Generator, error) {
|
|
||||||
// skip loading plugin if path is a directory with no 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// try and open the plugin
|
|
||||||
p, err := plugin.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open plugin: %w", 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert that the plugin loaded has a valid generator
|
|
||||||
gen, ok := symbol.(Generator)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("failed to load the correct symbol type at path '%s'", path)
|
|
||||||
}
|
|
||||||
return gen, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads all generator plugins in a given directory.
|
|
||||||
//
|
|
||||||
// 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) {
|
|
||||||
// check if verbose option is supplied
|
|
||||||
var (
|
|
||||||
generators = make(map[string]Generator)
|
|
||||||
params = ToParams(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gen.Generate(config, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main function to generate a collection of files as a map with the path as the key and
|
|
||||||
// the contents of the file as the value. This function currently expects a list of plugin
|
|
||||||
// paths to load all plugins within a directory. Then, each plugin's generator.GenerateWithTarget()
|
|
||||||
// function is called for each target specified.
|
|
||||||
//
|
|
||||||
// This function is the corresponding implementation for the "generate" CLI subcommand.
|
|
||||||
// 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) {
|
|
||||||
// load generator plugins to generate configs or to print
|
|
||||||
var (
|
|
||||||
opts []client.Option
|
|
||||||
targetInfo configurator.Target
|
|
||||||
generator Generator
|
|
||||||
params Params
|
|
||||||
err error
|
|
||||||
ok bool
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// run the generator plugin from target passed
|
|
||||||
return generator.Generate(config, params)
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package generator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Hostfile struct{}
|
|
||||||
|
|
||||||
func (g *Hostfile) GetName() string {
|
|
||||||
return "hostfile"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Hostfile) GetVersion() string {
|
|
||||||
return util.GitCommit()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return nil, fmt.Errorf("plugin does not implement generation function")
|
|
||||||
}
|
|
||||||
|
|
@ -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]
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package generator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Powerman struct{}
|
|
||||||
|
|
||||||
func (g *Powerman) GetName() string {
|
|
||||||
return "powerman"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Powerman) GetVersion() string {
|
|
||||||
return util.GitCommit()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return nil, fmt.Errorf("plugin does not implement generation function")
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package generator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
|
||||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Syslog struct{}
|
|
||||||
|
|
||||||
func (g *Syslog) GetName() string {
|
|
||||||
return "syslog"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Syslog) GetVersion() string {
|
|
||||||
return util.GitCommit()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return nil, fmt.Errorf("plugin does not implement generation function")
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
119
pkg/log/log.go
Normal file
119
pkg/log/log.go
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// string representation that directly corresponds to zerolog.Level
|
||||||
|
type LogLevel string
|
||||||
|
type LogLevelList []LogLevel
|
||||||
|
type LogFilter string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEBUG LogLevel = "debug"
|
||||||
|
INFO LogLevel = "info"
|
||||||
|
WARN LogLevel = "warn"
|
||||||
|
ERROR LogLevel = "error"
|
||||||
|
DISABLED LogLevel = "disabled"
|
||||||
|
TRACE LogLevel = "trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Levels = [6]LogLevel{DEBUG, INFO, WARN, ERROR, DISABLED, TRACE}
|
||||||
|
var LogFile *os.File
|
||||||
|
|
||||||
|
func (ll LogLevel) String() string {
|
||||||
|
return string(ll)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ll *LogLevel) Set(v string) error {
|
||||||
|
switch LogLevel(v) {
|
||||||
|
case DEBUG, INFO, WARN, ERROR, DISABLED, TRACE:
|
||||||
|
*ll = LogLevel(v)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("must be one of %v", []LogLevel{
|
||||||
|
DEBUG,
|
||||||
|
INFO,
|
||||||
|
WARN,
|
||||||
|
ERROR,
|
||||||
|
DISABLED,
|
||||||
|
TRACE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (df LogLevel) Type() string {
|
||||||
|
return "LogLevel"
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitWithLogLevel(logLevel LogLevel, logPath string) error {
|
||||||
|
var (
|
||||||
|
logger zerolog.Logger
|
||||||
|
level zerolog.Level
|
||||||
|
writer zerolog.LevelWriter
|
||||||
|
writers []io.Writer
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// set the logging level
|
||||||
|
level, err = strToLogLevel(logLevel)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to convert log level: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the default stderr writer
|
||||||
|
writers = append(writers, &zerolog.FilteredLevelWriter{
|
||||||
|
Writer: &zerolog.LevelWriterAdapter{os.Stderr},
|
||||||
|
Level: level,
|
||||||
|
})
|
||||||
|
|
||||||
|
// add another writer to write to a log file
|
||||||
|
if logPath != "" {
|
||||||
|
LogFile, err = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open log file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add another write to write to the specified log file
|
||||||
|
writers = append(writers, &zerolog.FilteredLevelWriter{
|
||||||
|
Writer: zerolog.LevelWriterAdapter{LogFile},
|
||||||
|
Level: level,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
writer = zerolog.MultiLevelWriter(writers...)
|
||||||
|
logger = zerolog.New(writer).Level(level).With().Timestamp().Caller().Logger()
|
||||||
|
zerolog.SetGlobalLevel(level)
|
||||||
|
log.Logger = logger
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func strToLogLevel(ll LogLevel) (zerolog.Level, error) {
|
||||||
|
var tostr = func(lls []LogLevel) []string {
|
||||||
|
s := []string{}
|
||||||
|
for _, l := range lls {
|
||||||
|
s = append(s, string(l))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
if index := slices.Index(Levels[:], ll); index >= 0 {
|
||||||
|
// handle special cases to map index to DISABLED and TRACE
|
||||||
|
switch index {
|
||||||
|
case 4:
|
||||||
|
return zerolog.Disabled, nil
|
||||||
|
case 5:
|
||||||
|
return zerolog.TraceLevel, nil
|
||||||
|
}
|
||||||
|
return zerolog.Level(index), nil
|
||||||
|
}
|
||||||
|
return -100, fmt.Errorf(
|
||||||
|
"invalid log level (options: %s)", strings.Join(tostr(Levels[:]), ", "),
|
||||||
|
) // use 'info' by default
|
||||||
|
}
|
||||||
52
pkg/models.go
Normal file
52
pkg/models.go
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package makeshift
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfileMap map[string]*Profile
|
||||||
|
type Profile struct {
|
||||||
|
ID string `json:"id"` // profile ID
|
||||||
|
Description string `json:"description,omitempty"` // profile description
|
||||||
|
Tags []string `json:"tags,omitempty"` // tags used for ...
|
||||||
|
Data map[string]any `json:"data,omitempty"` // include render data
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plugin interface {
|
||||||
|
Name() string
|
||||||
|
Version() string
|
||||||
|
Description() string
|
||||||
|
Metadata() Metadata
|
||||||
|
|
||||||
|
Init() error
|
||||||
|
Run(data storage.KVStore, args []string) error
|
||||||
|
Cleanup() error
|
||||||
|
}
|
||||||
|
type Metadata map[string]any
|
||||||
|
type Hook struct {
|
||||||
|
Data storage.KVStore
|
||||||
|
Args []string
|
||||||
|
Plugin Plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hook) Init() error {
|
||||||
|
return h.Plugin.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hook) Run() error {
|
||||||
|
return h.Plugin.Run(h.Data, h.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hook) Cleanup() error {
|
||||||
|
return h.Plugin.Cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func PluginToMap(p Plugin) map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"name": p.Name(),
|
||||||
|
"version": p.Version(),
|
||||||
|
"description": p.Description(),
|
||||||
|
"metadata": p.Metadata(),
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
128
pkg/plugins/jinja2/jinja2.go
Normal file
128
pkg/plugins/jinja2/jinja2.go
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/storage"
|
||||||
|
"github.com/nikolalohinski/gonja/v2"
|
||||||
|
"github.com/nikolalohinski/gonja/v2/exec"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Jinja2 struct{}
|
||||||
|
|
||||||
|
func (p *Jinja2) Name() string { return "jinja2" }
|
||||||
|
func (p *Jinja2) Version() string { return "v0.0.1-alpha" }
|
||||||
|
func (p *Jinja2) Description() string { return "Renders Jinja 2 templates" }
|
||||||
|
func (p *Jinja2) Metadata() makeshift.Metadata {
|
||||||
|
return makeshift.Metadata{
|
||||||
|
"author": map[string]any{
|
||||||
|
"name": "David J. Allen",
|
||||||
|
"email": "davidallendj@gmail.com",
|
||||||
|
"links": []string{
|
||||||
|
"https://github.com/davidallendj",
|
||||||
|
"https://git.towk2.me/towk",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Jinja2) Init() error {
|
||||||
|
// nothing to initialize
|
||||||
|
log.Debug().Str("plugin", p.Name()).Msg("jinja2.Init()")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Jinja2) Run(store storage.KVStore, args []string) error {
|
||||||
|
// render the files using Jinja 2 from args
|
||||||
|
var (
|
||||||
|
mappings struct {
|
||||||
|
Data map[string]any `json:"data"`
|
||||||
|
}
|
||||||
|
context *exec.Context
|
||||||
|
template *exec.Template
|
||||||
|
profiles any // makeshift.ProfileMap
|
||||||
|
input any // []byte
|
||||||
|
output bytes.Buffer
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
log.Debug().
|
||||||
|
Str("plugin", p.Name()).
|
||||||
|
Any("store", store).
|
||||||
|
Strs("args", args).
|
||||||
|
Int("arg_count", len(args)).
|
||||||
|
Msg("(jinja2) Run()")
|
||||||
|
|
||||||
|
profiles, err = store.Get("profiles")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(jinja2) failed to get profiles: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err = store.Get("file")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(jinja2) failed to get input data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the templates provided as args to the plugin
|
||||||
|
template, err = gonja.FromBytes(input.([]byte))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(jinja2) failed to get template from args: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get mappings from shared data (optional)
|
||||||
|
shared, err := store.Get("shared")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("(jinja2) could not retrieve shared data")
|
||||||
|
} else {
|
||||||
|
err = json.Unmarshal(shared.([]byte), &mappings)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(jinja2) failed to unmarshal mappings from shared data: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ps = make(map[string]any)
|
||||||
|
for profileID, profile := range profiles.(makeshift.ProfileMap) {
|
||||||
|
ps[profileID] = map[string]any{
|
||||||
|
"id": profile.ID,
|
||||||
|
"description": profile.Description,
|
||||||
|
"data": profile.Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inject profiles and plugin-specific mapping
|
||||||
|
mappings.Data = map[string]any{
|
||||||
|
"makeshift": map[string]any{
|
||||||
|
"profiles": ps,
|
||||||
|
"plugin": map[string]any{
|
||||||
|
"name": p.Name(),
|
||||||
|
"version": p.Version(),
|
||||||
|
"description": p.Description(),
|
||||||
|
"metadata": p.Metadata(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Any("mappings", mappings).Send()
|
||||||
|
|
||||||
|
// use the provided data in the store to render templates
|
||||||
|
// NOTE: this may be changed to specifically use "shared" data instead
|
||||||
|
context = exec.NewContext(mappings.Data)
|
||||||
|
if err = template.Execute(&output, context); err != nil { // Prints: Hello Bob!
|
||||||
|
return fmt.Errorf("(jinja2) failed to render template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write render templates to data store output
|
||||||
|
store.Set("out", output.Bytes())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Jinja2) Cleanup() error {
|
||||||
|
// nothing to clean up
|
||||||
|
log.Debug().Str("plugin", p.Name()).Msg("(jinja2) Cleanup()")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var Makeshift Jinja2
|
||||||
30
pkg/plugins/mapper/mapper.go
Normal file
30
pkg/plugins/mapper/mapper.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.towk2.me/towk/makeshift/pkg/storage"
|
||||||
|
|
||||||
|
type Mapper struct{}
|
||||||
|
|
||||||
|
func (p *Mapper) Name() string { return "jinja2" }
|
||||||
|
func (p *Mapper) Version() string { return "test" }
|
||||||
|
func (p *Mapper) Description() string { return "Renders Jinja 2 templates" }
|
||||||
|
func (p *Mapper) Metadata() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"author.name": "David J. Allen",
|
||||||
|
"author.email": "davidallendj@gmail.com",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Mapper) Init() error {
|
||||||
|
// nothing to initialize
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Mapper) Run(data storage.KVStore, args []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Mapper) Clean() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var Makeshift Mapper
|
||||||
279
pkg/plugins/smd/smd.go
Normal file
279
pkg/plugins/smd/smd.go
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/storage"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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:"-"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
AccessToken string `yaml:"access-token"`
|
||||||
|
|
||||||
|
RedfishEndpoints []RedfishEndpoint `json:"redfish_endpoints"`
|
||||||
|
EthernetInterfaces []EthernetInterface `json:"ethernet_interfaces"`
|
||||||
|
Components []Component `json:"components"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPAddr struct {
|
||||||
|
IpAddress string `json:"IPAddress"`
|
||||||
|
Network string `json:"Network"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EthernetInterface struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
Description string `json:"Description"`
|
||||||
|
MacAddress string `json:"MACAddr"`
|
||||||
|
LastUpdate string `json:"LastUpdate"`
|
||||||
|
ComponentID string `json:"ComponentID"`
|
||||||
|
Type string `json:"Type"`
|
||||||
|
IpAddresses []IPAddr `json:"IPAddresses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Component struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
Type string `json:"Type"`
|
||||||
|
State string `json:"State,omitempty"`
|
||||||
|
Flag string `json:"Flag,omitempty"`
|
||||||
|
Enabled *bool `json:"Enabled,omitempty"`
|
||||||
|
SwStatus string `json:"SoftwareStatus,omitempty"`
|
||||||
|
Role string `json:"Role,omitempty"`
|
||||||
|
SubRole string `json:"SubRole,omitempty"`
|
||||||
|
NID json.Number `json:"NID,omitempty"`
|
||||||
|
Subtype string `json:"Subtype,omitempty"`
|
||||||
|
NetType string `json:"NetType,omitempty"`
|
||||||
|
Arch string `json:"Arch,omitempty"`
|
||||||
|
Class string `json:"Class,omitempty"`
|
||||||
|
ReservationDisabled bool `json:"ReservationDisabled,omitempty"`
|
||||||
|
Locked bool `json:"Locked,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedfishEndpoint struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
Type string `json:"Type"`
|
||||||
|
Name string `json:"Name,omitempty"` // user supplied descriptive name
|
||||||
|
Hostname string `json:"Hostname"`
|
||||||
|
Domain string `json:"Domain"`
|
||||||
|
FQDN string `json:"FQDN"`
|
||||||
|
Enabled bool `json:"Enabled"`
|
||||||
|
UUID string `json:"UUID,omitempty"`
|
||||||
|
User string `json:"User"`
|
||||||
|
Password string `json:"Password"` // Temporary until more secure method
|
||||||
|
UseSSDP bool `json:"UseSSDP,omitempty"`
|
||||||
|
MACRequired bool `json:"MACRequired,omitempty"`
|
||||||
|
MACAddr string `json:"MACAddr,omitempty"`
|
||||||
|
IPAddr string `json:"IPAddress,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SmdClient) Name() string { return "smd" }
|
||||||
|
func (p *SmdClient) Version() string { return "v0.0.1-alpha" }
|
||||||
|
func (p *SmdClient) Description() string { return "Fetchs data from SMD and writes to store" }
|
||||||
|
func (p *SmdClient) Metadata() makeshift.Metadata {
|
||||||
|
return makeshift.Metadata{
|
||||||
|
"author": map[string]any{
|
||||||
|
"name": "David J. Allen",
|
||||||
|
"email": "davidallendj@gmail.com",
|
||||||
|
"links": []string{
|
||||||
|
"https://github.com/davidallendj",
|
||||||
|
"https://git.towk2.me/towk",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SmdClient) Init() error {
|
||||||
|
log.Debug().Str("plugin", p.Name()).Msg("(smd) Init()")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SmdClient) Run(store storage.KVStore, args []string) error {
|
||||||
|
// set all the defaults for variables
|
||||||
|
var (
|
||||||
|
client SmdClient
|
||||||
|
bytes []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("plugin", p.Name()).
|
||||||
|
Strs("args", args).
|
||||||
|
Int("arg_count", len(args)).
|
||||||
|
Any("store", store).
|
||||||
|
Msg("(smd) Run()")
|
||||||
|
|
||||||
|
// if we have a client, try making the request for the ethernet interfaces
|
||||||
|
err = client.FetchEthernetInterfaces()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(smd) failed to fetch ethernet interfaces with client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.FetchRedfishEndpoints()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(smd) failed to fetch redfish endpoints with client: %v", err)
|
||||||
|
}
|
||||||
|
err = client.FetchComponents()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(smd) failed to fetch components with client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write data back to shared data store to be used by other plugins
|
||||||
|
bytes, err = json.Marshal(client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(smd) failed to marshal SMD client: %v", err)
|
||||||
|
}
|
||||||
|
store.Set("shared", bytes)
|
||||||
|
|
||||||
|
// apply template substitutions and return output as byte array
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SmdClient) Cleanup() error {
|
||||||
|
log.Debug().Str("plugin", p.Name()).Msg("(smd) Init()")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() error {
|
||||||
|
var (
|
||||||
|
bytes []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
// make request to SMD endpoint
|
||||||
|
bytes, err = client.makeRequest("/Inventory/EthernetInterfaces")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read HTTP response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal response body JSON and extract in object
|
||||||
|
err = json.Unmarshal(bytes, &client.EthernetInterfaces)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print what we got if verbose is set
|
||||||
|
log.Debug().Str("ethernet_interfaces", string(bytes)).Msg("found interfaces")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() error {
|
||||||
|
var (
|
||||||
|
bytes []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
// make request to SMD endpoint
|
||||||
|
bytes, err = client.makeRequest("/State/Components")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure our response is actually JSON first
|
||||||
|
if !json.Valid(bytes) {
|
||||||
|
return fmt.Errorf("expected valid JSON response: %v", string(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal response body JSON and extract in object
|
||||||
|
var tmp map[string]any
|
||||||
|
err = json.Unmarshal(bytes, &tmp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
bytes, err = json.Marshal(tmp["RedfishEndpoints"].([]any))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal JSON: %v", err)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(bytes, &client.Components)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print what we got if verbose is set
|
||||||
|
log.Debug().Str("components", string(bytes)).Msg("found components")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: improve implementation of this function
|
||||||
|
func (client *SmdClient) FetchRedfishEndpoints() error {
|
||||||
|
var (
|
||||||
|
store map[string]any
|
||||||
|
rfeps []RedfishEndpoint
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// make initial request to get JSON with 'RedfishEndpoints' as property
|
||||||
|
body, err = client.makeRequest("/Inventory/RedfishEndpoints")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make HTTP resquest: %v", err)
|
||||||
|
}
|
||||||
|
// make sure response is in JSON
|
||||||
|
if !json.Valid(body) {
|
||||||
|
return fmt.Errorf("expected valid JSON response: %s", string(body))
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &store)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshal RedfishEndpoint JSON back to makeshift.RedfishEndpoint
|
||||||
|
body, err = json.Marshal(store["RedfishEndpoints"].([]any))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal JSON: %v", err)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &rfeps)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// show the final result
|
||||||
|
log.Debug().Bytes("redfish_endpoints", body).Msg("found redfish endpoints")
|
||||||
|
|
||||||
|
client.RedfishEndpoints = rfeps
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) {
|
||||||
|
if client == nil {
|
||||||
|
return nil, fmt.Errorf("client is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch DHCP related information from SMD's endpoint:
|
||||||
|
url := fmt.Sprintf("%s/hsm/v2%s", client.Host, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// include access token in authorzation header if found
|
||||||
|
// NOTE: This shouldn't be needed for this endpoint since it's public
|
||||||
|
if client.AccessToken != "" {
|
||||||
|
req.Header.Add("Authorization", "Bearer "+client.AccessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make the request to SMD
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the contents of the response body
|
||||||
|
return io.ReadAll(res.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var Makeshift SmdClient
|
||||||
|
|
@ -1,327 +0,0 @@
|
||||||
//go:build server || all
|
|
||||||
// +build server all
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
openchami_authenticator "github.com/openchami/chi-middleware/auth"
|
|
||||||
openchami_logger "github.com/openchami/chi-middleware/log"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
tokenAuth *jwtauth.JWTAuth = nil
|
|
||||||
)
|
|
||||||
|
|
||||||
type Jwks struct {
|
|
||||||
Uri string
|
|
||||||
Retries int
|
|
||||||
}
|
|
||||||
type Server struct {
|
|
||||||
*http.Server
|
|
||||||
Config *config.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 {
|
|
||||||
// create default config if none supplied
|
|
||||||
if conf == nil {
|
|
||||||
c := config.New()
|
|
||||||
conf = &c
|
|
||||||
}
|
|
||||||
newServer := &Server{
|
|
||||||
Config: conf,
|
|
||||||
Server: &http.Server{Addr: conf.Server.Host},
|
|
||||||
Jwks: Jwks{
|
|
||||||
Uri: conf.Server.Jwks.Uri,
|
|
||||||
Retries: conf.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})
|
|
||||||
|
|
||||||
// set the server address with config values
|
|
||||||
s.Server.Addr = s.Config.Server.Host
|
|
||||||
|
|
||||||
// fetch JWKS public key from authorization server
|
|
||||||
if s.Config.Server.Jwks.Uri != "" && tokenAuth == nil {
|
|
||||||
for i := 0; i < s.Config.Server.Jwks.Retries; i++ {
|
|
||||||
var err error
|
|
||||||
tokenAuth, err = configurator.FetchPublicKeyFromURL(s.Config.Server.Jwks.Uri)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msgf("failed to fetch JWKS")
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new go-chi router with its routes
|
|
||||||
router := chi.NewRouter()
|
|
||||||
router.Use(middleware.RequestID)
|
|
||||||
router.Use(middleware.RealIP)
|
|
||||||
router.Use(middleware.Logger)
|
|
||||||
router.Use(middleware.Recoverer)
|
|
||||||
router.Use(middleware.StripSlashes)
|
|
||||||
router.Use(middleware.Timeout(60 * time.Second))
|
|
||||||
router.Use(openchami_logger.OpenCHAMILogger(logger))
|
|
||||||
if s.Config.Server.Jwks.Uri != "" {
|
|
||||||
router.Group(func(r chi.Router) {
|
|
||||||
r.Use(
|
|
||||||
jwtauth.Verifier(tokenAuth),
|
|
||||||
openchami_authenticator.AuthenticatorWithRequiredClaims(tokenAuth, []string{"sub", "iss", "aud"}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// protected routes if using auth
|
|
||||||
r.HandleFunc("/generate", s.Generate(opts...))
|
|
||||||
r.Post("/targets", s.createTarget)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// public routes without auth
|
|
||||||
router.HandleFunc("/generate", s.Generate(opts...))
|
|
||||||
router.Post("/targets", s.createTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
// always available public routes go here (none at the moment)
|
|
||||||
router.HandleFunc("/configurator/status", s.GetStatus)
|
|
||||||
|
|
||||||
s.Handler = router
|
|
||||||
return s.ListenAndServe()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement a way to shut the server down
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to encode JSON: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new target with name, generator, templates, and files.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// curl -X POST /target?name=test&plugin=dnsmasq
|
|
||||||
//
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(w, "failed to read response body: %v", err)
|
|
||||||
log.Error().Err(err).Msg("failed to read response body")
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
77
pkg/service/constants.go
Normal file
77
pkg/service/constants.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
const (
|
||||||
|
RELPATH_PLUGINS = "/plugins"
|
||||||
|
RELPATH_PROFILES = "/profiles"
|
||||||
|
RELPATH_DATA = "/data"
|
||||||
|
RELPATH_METADATA = "/.makeshift"
|
||||||
|
RELPATH_HELP = RELPATH_DATA + "/index.html"
|
||||||
|
RELPATH_PROFILE = RELPATH_PROFILES + "/default.json"
|
||||||
|
|
||||||
|
PATH_CONFIG = "$HOME/.config/makeshift/config.yaml"
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT_IN_SECS = 60
|
||||||
|
DEFAULT_PLUGINS_MAX_COUNT = 32
|
||||||
|
DEFAULT_PROFILES_MAX_COUNT = 256
|
||||||
|
|
||||||
|
FILE_METADATA = ``
|
||||||
|
FILE_HOME_PAGE = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
Plugin Information:
|
||||||
|
Name: {{ makeshift.plugin.name }}
|
||||||
|
Version: {{ makeshift.plugin.version }}
|
||||||
|
Description: {{ makeshift.plugin.description }}
|
||||||
|
Author: {{ makeshift.plugin.metadata.name }} ({{ makeshift.plugin.metadata.email }})
|
||||||
|
|
||||||
|
Profile Information:
|
||||||
|
ID: {{ makeshift.profiles.default.id }}
|
||||||
|
Description: {{ makeshift.profiles.default.description }}
|
||||||
|
|
||||||
|
# setup environment variables</br>
|
||||||
|
export MAKESHIFT_HOST={{ makeshift.profiles.default.data.host }}</br>
|
||||||
|
export MAKESHIFT_PATH={{ makeshift.profiles.default.data.path }}</br>
|
||||||
|
export MAKESHIFT_ROOT={{ makeshift.profiles.default.data.server_root }}</br></br>
|
||||||
|
|
||||||
|
# start the service</br>
|
||||||
|
makeshift serve --root ./tests --init -l debug</br></br>
|
||||||
|
|
||||||
|
# download a file or directory (as archive)</br>
|
||||||
|
makeshift download</br>
|
||||||
|
makeshift download --host http://localhost:5050 --path help.txt</br></br>
|
||||||
|
|
||||||
|
# download files with rendering using plugins</br>
|
||||||
|
makeshift download --plugins smd,jinja2 --profile compute</br>
|
||||||
|
makeshift download -p templates --plugins jinja --profile io</br>
|
||||||
|
curl $MAKESHIFT_HOST/download/help.txt?plugins=smd,jinja2</br></br>
|
||||||
|
|
||||||
|
# upload a file or directory (recursively)</br>
|
||||||
|
makeshift upload</br>
|
||||||
|
makeshift upload --host http://localhost:5050 --path help.txt</br></br>
|
||||||
|
|
||||||
|
# list the files in a directory</br>
|
||||||
|
makeshift list --path help.txt</br>
|
||||||
|
makeshift list --host http://localhost:5050 --path help.txt</br>
|
||||||
|
curl http://localhost:5050/list/test</br>
|
||||||
|
</p>
|
||||||
|
<body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
FILE_DEFAULT_PROFILE = `
|
||||||
|
{
|
||||||
|
"id": "default",
|
||||||
|
"description": "Makeshift default profile",
|
||||||
|
"data": {
|
||||||
|
"host": "localhost",
|
||||||
|
"path": "/test",
|
||||||
|
"server_root": "./test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
// makeshift.host: https://localhost:5050
|
||||||
|
// makeshift.path: test
|
||||||
|
// makeshift.server.root: $HOME/apps/makeshift
|
||||||
141
pkg/service/plugins.go
Normal file
141
pkg/service/plugins.go
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) ListPlugins() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
plugins map[string]makeshift.Plugin
|
||||||
|
names []string
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
plugins, err = LoadPluginsFromDir(s.PathForPlugins())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range plugins {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err = json.Marshal(names)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetPluginInfo() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
pluginName = chi.URLParam(r, "name")
|
||||||
|
path = s.PathForPluginWithName(pluginName)
|
||||||
|
plugin makeshift.Plugin
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
plugin, err = LoadPluginFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err = json.Marshal(makeshift.PluginToMap(plugin))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetPluginRaw() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
pluginName = chi.URLParam(r, "name")
|
||||||
|
path = s.PathForPluginWithName(pluginName)
|
||||||
|
rawPlugin []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
rawPlugin, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(rawPlugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreatePlugin() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
pluginName = chi.URLParam(r, "name")
|
||||||
|
body []byte
|
||||||
|
path string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// helper to check for valid plugin name
|
||||||
|
var hasValidName = func(name string) bool {
|
||||||
|
return name != "" && len(name) < 64
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for a valid plugin name
|
||||||
|
if !hasValidName(pluginName) {
|
||||||
|
http.Error(w, "invalid name for plugin", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err = io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path = s.PathForPluginWithName(pluginName)
|
||||||
|
err = os.WriteFile(path, body, 0o777)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeletePlugin() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
pluginName = chi.URLParam(r, "name")
|
||||||
|
path = s.PathForPluginWithName(pluginName)
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug().Str("path", path).Send()
|
||||||
|
err = os.Remove(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
278
pkg/service/profiles.go
Normal file
278
pkg/service/profiles.go
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) ListProfiles() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
path = s.RootPath + RELPATH_PROFILES
|
||||||
|
profiles []*makeshift.Profile
|
||||||
|
contents []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// walk profiles directory to load all profiles
|
||||||
|
err = filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip directories
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// read file contents
|
||||||
|
var profile *makeshift.Profile
|
||||||
|
profile, err = LoadProfileFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles = append(profiles, profile)
|
||||||
|
|
||||||
|
fmt.Println(path, info.Size())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshal and send all the profiles
|
||||||
|
contents, err = json.Marshal(profiles)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to marshal profiles: %v", err), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(contents)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetProfile() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
id = chi.URLParam(r, "id")
|
||||||
|
path = s.PathForProfileWithID(id)
|
||||||
|
contents []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
contents, err = loadProfileContents(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(contents)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateProfile() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
body, contents []byte
|
||||||
|
path string
|
||||||
|
profile *makeshift.Profile
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
body, err = io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the request info to build profile
|
||||||
|
err = json.Unmarshal(body, &profile)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to unmarshal profile: %v", err.Error()), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// serialize just the profile part
|
||||||
|
contents, err = json.Marshal(profile)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to marshal profile: %v", err.Error()), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new profile on disk
|
||||||
|
path = s.PathForProfileWithID(profile.ID)
|
||||||
|
err = os.WriteFile(path, contents, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteProfile() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
profileID = chi.URLParam(r, "id")
|
||||||
|
path string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if profileID == "default" {
|
||||||
|
http.Error(w, "cannot delete the default profile", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path = s.PathForProfileWithID(profileID)
|
||||||
|
err = os.Remove(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetProfileData() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
body, contents []byte
|
||||||
|
newContents string
|
||||||
|
profile *makeshift.Profile
|
||||||
|
path string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
body, err = io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &profile)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the request data sets an ID
|
||||||
|
if profile.ID == "" {
|
||||||
|
http.Error(w, "ID must be set to a non-empty value", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the contents the file with profile ID
|
||||||
|
path = s.PathForProfileWithID(profile.ID)
|
||||||
|
contents, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// modify the data of the profile's contents
|
||||||
|
newContents, err = sjson.Set(string(contents), "data", profile.Data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write only the data to the file with ID
|
||||||
|
err = os.WriteFile(path, []byte(newContents), os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteProfileData() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
id = chi.URLParam(r, "id")
|
||||||
|
path = s.PathForProfileWithID(id)
|
||||||
|
profile *makeshift.Profile
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// get the profile
|
||||||
|
profile, err = LoadProfileFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the profile data
|
||||||
|
profile.Data = map[string]any{}
|
||||||
|
|
||||||
|
// save the profile back to the file to update
|
||||||
|
SaveProfileToFile(path, profile)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetProfileData() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
id = chi.URLParam(r, "id")
|
||||||
|
path = s.PathForProfileWithID(id)
|
||||||
|
profile *makeshift.Profile
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
profile, err = LoadProfileFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// only marshal the profile data and not entire profile
|
||||||
|
body, err = json.Marshal(profile.Data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write body to response
|
||||||
|
_, err = w.Write(body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadProfileContents(path string) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
contents []byte
|
||||||
|
profile *makeshift.Profile
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
profile, err = LoadProfileFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load profile from file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contents, err = json.Marshal(profile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal profile: %v", err)
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
376
pkg/service/routes.go
Normal file
376
pkg/service/routes.go
Normal file
|
|
@ -0,0 +1,376 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.towk2.me/towk/makeshift/internal/archive"
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/storage"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) Download() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
path = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/download")
|
||||||
|
pluginNames = strings.Split(r.URL.Query().Get("plugins"), ",")
|
||||||
|
profileIDs = strings.Split(r.URL.Query().Get("profiles"), ",")
|
||||||
|
|
||||||
|
fileInfo os.FileInfo
|
||||||
|
out *os.File
|
||||||
|
store *storage.MemoryStorage = new(storage.MemoryStorage)
|
||||||
|
hooks []makeshift.Hook
|
||||||
|
contents []byte
|
||||||
|
errs []error
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// initialize storage
|
||||||
|
store.Init()
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("path", path).
|
||||||
|
Str("client_host", r.Host).
|
||||||
|
Strs("plugins", pluginNames).
|
||||||
|
Strs("profiles", profileIDs).
|
||||||
|
Any("query", r.URL.Query()).
|
||||||
|
Msg("Service.Download()")
|
||||||
|
|
||||||
|
// prepare profiles
|
||||||
|
errs = s.loadProfiles(profileIDs, store, errs)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
log.Error().Errs("errs", errs).Msg("errors occurred loading profiles")
|
||||||
|
errs = []error{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine if path is directory, file, or exists
|
||||||
|
if fileInfo, err = os.Stat(path); err == nil {
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
// get the final archive path
|
||||||
|
archivePath := fmt.Sprintf("%s.tar.gz", path)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("archive_path", archivePath).
|
||||||
|
Str("type", "directory").
|
||||||
|
Msg("Service.Download()")
|
||||||
|
|
||||||
|
out, err = os.Create(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
s.writeErrorResponse(w, fmt.Sprintf("failed to create named file: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a list of filenames to archive
|
||||||
|
filenamesToArchive := []string{}
|
||||||
|
filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if !d.IsDir() {
|
||||||
|
filenamesToArchive = append(filenamesToArchive, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
log.Debug().Strs("files", filenamesToArchive).Send()
|
||||||
|
|
||||||
|
// prepare plugins
|
||||||
|
hooks, errs = s.loadPlugins(pluginNames, store, nil, errs)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
log.Error().Errs("errs", errs).Msg("errors occurred loading plugins")
|
||||||
|
errs = []error{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an archive of the directory, run hooks, and download
|
||||||
|
err = archive.Create(filenamesToArchive, out, hooks)
|
||||||
|
if err != nil {
|
||||||
|
s.writeErrorResponse(w, fmt.Sprintf("failed to create archive: %v", err.Error()), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the final archive
|
||||||
|
contents, err = os.ReadFile(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
s.writeErrorResponse(w, fmt.Sprintf("failed to read archive contents: %v", err.Error()), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send the archive back as response
|
||||||
|
w.Header().Add("FILETYPE", "archive")
|
||||||
|
w.Write(contents)
|
||||||
|
|
||||||
|
// clean up the temporary archive
|
||||||
|
err = os.Remove(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to remove temporary archive")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// download individual file
|
||||||
|
log.Debug().
|
||||||
|
Str("type", "file").
|
||||||
|
Msg("Service.Download()")
|
||||||
|
|
||||||
|
contents, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
s.writeErrorResponse(w, fmt.Sprintf("failed to read file to download: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare plugins
|
||||||
|
|
||||||
|
store.Set("file", contents)
|
||||||
|
hooks, errs = s.loadPlugins(pluginNames, store, nil, errs)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
log.Error().Errs("errs", errs).Msg("errors occurred loading plugins")
|
||||||
|
errs = []error{}
|
||||||
|
}
|
||||||
|
if len(hooks) > 0 {
|
||||||
|
// run pre-hooks to modify the contents of the file before archiving
|
||||||
|
log.Debug().Int("hook_count", len(hooks)).Msg("running hooks")
|
||||||
|
for _, hook := range hooks {
|
||||||
|
log.Debug().Any("hook", map[string]any{
|
||||||
|
"store": hook.Data,
|
||||||
|
"args": hook.Args,
|
||||||
|
"plugin": map[string]string{
|
||||||
|
"name": hook.Plugin.Name(),
|
||||||
|
"description": hook.Plugin.Description(),
|
||||||
|
"version": hook.Plugin.Version(),
|
||||||
|
},
|
||||||
|
}).Send()
|
||||||
|
err = hook.Init()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("plugin", hook.Plugin.Name()).
|
||||||
|
Msg("failed to initialize plugin")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = hook.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("plugin", hook.Plugin.Name()).
|
||||||
|
Msg("failed to run plugin")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = hook.Cleanup()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("plugin", hook.Plugin.Name()).
|
||||||
|
Msg("failed to cleanup plugin")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// take the contents from the last hook and update files
|
||||||
|
var (
|
||||||
|
hook = hooks[len(hooks)-1]
|
||||||
|
data any
|
||||||
|
)
|
||||||
|
data, err = hook.Data.Get("out")
|
||||||
|
if err != nil {
|
||||||
|
s.writeErrorResponse(w, fmt.Sprintf("failed to get data from hook: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send processed (with plugins) file back as response
|
||||||
|
w.Header().Add("FILETYPE", "file")
|
||||||
|
w.Write(data.([]byte))
|
||||||
|
} else {
|
||||||
|
// send non-processed file back as response
|
||||||
|
w.Header().Add("FILETYPE", "file")
|
||||||
|
w.Write(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.writeErrorResponse(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Upload() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
path = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/upload")
|
||||||
|
body []byte
|
||||||
|
dirpath string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// show what we're uploading
|
||||||
|
log.Debug().
|
||||||
|
Str("path", path).
|
||||||
|
Msg("Service.Upload()")
|
||||||
|
|
||||||
|
// take the provided path and store the file contents
|
||||||
|
dirpath = filepath.Dir(path)
|
||||||
|
err = os.MkdirAll(dirpath, 0o777)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write file to disk
|
||||||
|
body, err = io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = os.WriteFile(path, body, 0o777)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) List() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
path = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/list")
|
||||||
|
entries []string
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// show what we're listing
|
||||||
|
log.Debug().Str("path", path).Msg("Service.List()")
|
||||||
|
|
||||||
|
// walk directory and show all entries "ls"
|
||||||
|
err = filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entries = append(entries, d.Name())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case fs.ErrNotExist, fs.ErrInvalid:
|
||||||
|
http.Error(w, "No such file or directory...", http.StatusBadRequest)
|
||||||
|
case fs.ErrPermission:
|
||||||
|
http.Error(w, "Invalid permissions...", http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Something went wrong (file or directory *probably* does not exist)...", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err = json.Marshal(entries)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
path = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/delete")
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = os.RemoveAll(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err := json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"code": http.StatusOK,
|
||||||
|
"message": "The makeshift server is healthy",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to encode JSON response body: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadProfiles(profileIDs []string, store storage.KVStore, errs []error) []error {
|
||||||
|
// load data from profiles into the data store
|
||||||
|
var profiles = make(makeshift.ProfileMap, len(profileIDs))
|
||||||
|
for i, profileID := range profileIDs {
|
||||||
|
var (
|
||||||
|
profilePath = s.PathForProfileWithID(profileID)
|
||||||
|
profile *makeshift.Profile
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if i > s.ProfilesMaxCount {
|
||||||
|
log.Warn().Msg("max profiles count reached...stopping")
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
if profileID == "" {
|
||||||
|
log.Warn().Msg("profile ID is empty...skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Debug().
|
||||||
|
Str("id", profileID).
|
||||||
|
Str("path", profilePath).
|
||||||
|
Msg("load profile")
|
||||||
|
profile, err = LoadProfileFromFile(profilePath)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
profiles[profileID] = profile
|
||||||
|
}
|
||||||
|
store.Set("profiles", profiles)
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadPlugins(pluginNames []string, store storage.KVStore, args []string, errs []error) ([]makeshift.Hook, []error) {
|
||||||
|
// create hooks to run from provided plugins specified
|
||||||
|
var hooks []makeshift.Hook
|
||||||
|
for i, pluginName := range pluginNames {
|
||||||
|
var (
|
||||||
|
pluginPath string = s.PathForPluginWithName(pluginName)
|
||||||
|
plugin makeshift.Plugin
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if i > s.PluginsMaxCount {
|
||||||
|
log.Warn().Msg("max plugins count reached or exceeded...stopping")
|
||||||
|
return hooks, errs
|
||||||
|
}
|
||||||
|
if pluginName == "" {
|
||||||
|
log.Warn().Msgf("no plugin name found with index %d...skipping", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Debug().
|
||||||
|
Str("name", pluginName).
|
||||||
|
Str("path", pluginPath).
|
||||||
|
Msg("load plugin")
|
||||||
|
|
||||||
|
// load the plugin from disk
|
||||||
|
plugin, err = LoadPluginFromFile(pluginPath)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hooks = append(hooks, makeshift.Hook{
|
||||||
|
Data: store,
|
||||||
|
Args: args,
|
||||||
|
Plugin: plugin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return hooks, errs
|
||||||
|
}
|
||||||
292
pkg/service/service.go
Normal file
292
pkg/service/service.go
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"plugin"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
makeshift "git.towk2.me/towk/makeshift/pkg"
|
||||||
|
"git.towk2.me/towk/makeshift/pkg/util"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
Addr string
|
||||||
|
RootPath string `yaml:"root,omitempty"`
|
||||||
|
CACertFile string `yaml:"cacert,omitempty"`
|
||||||
|
CACertKeyfile string `yaml:"keyfile,omitempty"`
|
||||||
|
|
||||||
|
// max counts
|
||||||
|
PluginsMaxCount int
|
||||||
|
ProfilesMaxCount int
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Service instance with default values
|
||||||
|
func New() *Service {
|
||||||
|
return &Service{
|
||||||
|
Addr: ":5050",
|
||||||
|
RootPath: "./",
|
||||||
|
PluginsMaxCount: DEFAULT_PLUGINS_MAX_COUNT,
|
||||||
|
ProfilesMaxCount: DEFAULT_PROFILES_MAX_COUNT,
|
||||||
|
Timeout: DEFAULT_TIMEOUT_IN_SECS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init() sets up the default files and directories for the service
|
||||||
|
func (s *Service) Init() error {
|
||||||
|
// create the default directories
|
||||||
|
var err error
|
||||||
|
err = os.MkdirAll(s.RootPath, 0o777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make service root path: %v", err)
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(s.PathForPlugins(), 0o777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make service plugin path: %v", err)
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(s.PathForProfiles(), 0o777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make service profile path: %v", err)
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(s.PathForData(), 0o777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make service data path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the default files
|
||||||
|
err = os.WriteFile(s.PathForMetadata(), []byte(FILE_METADATA), 0o777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make service metadata file: %v", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(s.PathForHome(), []byte(FILE_HOME_PAGE), 0o777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make service home page file: %v", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(s.PathForProfileWithID("default"), []byte(FILE_DEFAULT_PROFILE), 0o777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make service default profile file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve() starts the makeshift service and waits for requests.
|
||||||
|
func (s *Service) Serve() error {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
router.Use(middleware.RequestID)
|
||||||
|
router.Use(middleware.RealIP)
|
||||||
|
router.Use(middleware.Logger)
|
||||||
|
router.Use(middleware.Recoverer)
|
||||||
|
router.Use(middleware.StripSlashes)
|
||||||
|
router.Use(middleware.Timeout(s.Timeout * time.Second))
|
||||||
|
|
||||||
|
if s.requireAuth() {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// general
|
||||||
|
router.Get("/download/*", s.Download())
|
||||||
|
router.Post("/upload/*", s.Upload())
|
||||||
|
router.Get("/list/*", s.List())
|
||||||
|
router.Delete("/delete/*", s.Delete())
|
||||||
|
|
||||||
|
// profiles
|
||||||
|
router.Get("/profiles", s.ListProfiles())
|
||||||
|
router.Get("/profiles/{id}", s.GetProfile())
|
||||||
|
router.Post("/profiles/{id}", s.CreateProfile())
|
||||||
|
router.Delete("/profiles/{id}", s.DeleteProfile())
|
||||||
|
router.Get("/profiles/{id}/data", s.GetProfileData())
|
||||||
|
router.Post("/profiles/{id}/data", s.SetProfileData())
|
||||||
|
router.Delete("/profiles/{id}/data", s.DeleteProfileData())
|
||||||
|
|
||||||
|
// plugins
|
||||||
|
router.Get("/plugins", s.ListPlugins())
|
||||||
|
router.Get("/plugins/{name}/info", s.GetPluginInfo())
|
||||||
|
router.Get("/plugins/{name}/raw", s.GetPluginRaw())
|
||||||
|
router.Post("/plugins/{name}", s.CreatePlugin())
|
||||||
|
router.Delete("/plugins/{name}", s.DeletePlugin())
|
||||||
|
}
|
||||||
|
|
||||||
|
// always available public routes go here
|
||||||
|
router.HandleFunc("/status", s.GetStatus)
|
||||||
|
if s.CACertFile != "" && s.CACertKeyfile != "" {
|
||||||
|
return http.ListenAndServeTLS(s.Addr, s.CACertFile, s.CACertKeyfile, router)
|
||||||
|
} else {
|
||||||
|
return http.ListenAndServe(s.Addr, router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) requireAuth() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) FetchJwks(uri string) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadProfileFromFile(path string) (*makeshift.Profile, error) {
|
||||||
|
return loadFromJSONFile[makeshift.Profile](path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPluginFromFile loads a single plugin given a single file path
|
||||||
|
func LoadPluginFromFile(path string) (makeshift.Plugin, error) {
|
||||||
|
var (
|
||||||
|
isDir bool
|
||||||
|
err error
|
||||||
|
loadedPlugin *plugin.Plugin
|
||||||
|
)
|
||||||
|
// skip loading plugin if path is a directory with no error
|
||||||
|
isDir, err = util.IsDirectory(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to test if plugin path is directory: %v", err)
|
||||||
|
} else if isDir {
|
||||||
|
return nil, fmt.Errorf("path is a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// try and open the plugin
|
||||||
|
loadedPlugin, err = plugin.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open plugin at path '%s': %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the "Target" symbol from plugin
|
||||||
|
symbol, err := loadedPlugin.Lookup("Makeshift")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to look up symbol at path '%s': %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert that the plugin is a valid makeshift.Plugin
|
||||||
|
target, ok := symbol.(makeshift.Plugin)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to assert the correct symbol type at path '%s'", path)
|
||||||
|
}
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPluginsFromDir loads all plugins in a given directory.
|
||||||
|
//
|
||||||
|
// Returns a map of plugins. Each plugin can be accessed by the name
|
||||||
|
// returned by the plugin.GetName() implemented.
|
||||||
|
func LoadPluginsFromDir(dirpath string) (map[string]makeshift.Plugin, error) {
|
||||||
|
// check if verbose option is supplied
|
||||||
|
var (
|
||||||
|
cps = make(map[string]makeshift.Plugin)
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// helper to check for valid extensions
|
||||||
|
var hasValidExt = func(path string) bool {
|
||||||
|
return slices.Contains([]string{".so", ".dylib", ".dll"}, filepath.Ext(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk all files in directory only loading *valid* plugins
|
||||||
|
err = filepath.Walk(dirpath, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
// skip trying to load generator plugin if directory or error
|
||||||
|
// only try loading if file has .so extension
|
||||||
|
if info.IsDir() || err != nil || !hasValidExt(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the plugin from current path
|
||||||
|
p, err := LoadPluginFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load plugin in directory '%s': %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// map each plugin by name for lookup
|
||||||
|
cps[p.Name()] = p
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to walk directory '%s': %v", dirpath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveProfileToFile(path string, profile *makeshift.Profile) error {
|
||||||
|
return saveToJSONFile(path, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SavePluginToFile(path string, plugin *makeshift.Plugin) error {
|
||||||
|
return saveToJSONFile(path, plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFromJSONFile[T any](path string) (*T, error) {
|
||||||
|
var (
|
||||||
|
res *T
|
||||||
|
contents []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
contents, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(contents, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal contents from JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveToJSONFile[T any](path string, data T) error {
|
||||||
|
var (
|
||||||
|
contents []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
contents, err = json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal data to JSON: %v", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(path, contents, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write JSON to file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PathForProfileWithID(id string) string {
|
||||||
|
return s.RootPath + RELPATH_PROFILES + "/" + id + ".json"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PathForPluginWithName(name string) string {
|
||||||
|
return s.RootPath + RELPATH_PLUGINS + "/" + name + ".so"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PathForProfiles() string {
|
||||||
|
return s.RootPath + RELPATH_PROFILES + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PathForPlugins() string {
|
||||||
|
return s.RootPath + RELPATH_PLUGINS + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PathForData() string {
|
||||||
|
return s.RootPath + RELPATH_DATA
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PathForMetadata() string {
|
||||||
|
return s.RootPath + RELPATH_METADATA
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PathForHome() string {
|
||||||
|
return s.RootPath + RELPATH_HELP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) writeErrorResponse(w http.ResponseWriter, message string, code int) {
|
||||||
|
http.Error(w, message, code)
|
||||||
|
log.Error().Msg(message)
|
||||||
|
}
|
||||||
23
pkg/storage/disk.go
Normal file
23
pkg/storage/disk.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
type DiskStorage struct{}
|
||||||
|
|
||||||
|
func (ds DiskStorage) Init() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds DiskStorage) Cleanup() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds DiskStorage) Get(k string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds DiskStorage) Set(k string, v any) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds DiskStorage) GetData() any {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
33
pkg/storage/memory.go
Normal file
33
pkg/storage/memory.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type MemoryStorage struct {
|
||||||
|
Data map[string]any `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MemoryStorage) Init() error {
|
||||||
|
ms.Data = map[string]any{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MemoryStorage) Cleanup() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MemoryStorage) Get(k string) (any, error) {
|
||||||
|
v, ok := ms.Data[k]
|
||||||
|
if ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("value '%s' does not exist", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MemoryStorage) Set(k string, v any) error {
|
||||||
|
ms.Data[k] = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MemoryStorage) GetData() any {
|
||||||
|
return ms.Data
|
||||||
|
}
|
||||||
19
pkg/storage/storage.go
Normal file
19
pkg/storage/storage.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
type KVStore interface {
|
||||||
|
Init() error
|
||||||
|
Cleanup() error
|
||||||
|
|
||||||
|
Get(k string) (any, error)
|
||||||
|
Set(k string, v any) error
|
||||||
|
GetData() any
|
||||||
|
}
|
||||||
|
|
||||||
|
type KVStaticStore[T any] interface {
|
||||||
|
Init() error
|
||||||
|
Cleanup() error
|
||||||
|
|
||||||
|
Get(k string) (T, error)
|
||||||
|
Set(k string, v T) error
|
||||||
|
GetData() T
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"compress/gzip"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -99,64 +97,3 @@ func CopyIf[T comparable](s []T, condition func(t T) bool) []T {
|
||||||
}
|
}
|
||||||
return f
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
11
tests/01-download-localhost.hurl
Normal file
11
tests/01-download-localhost.hurl
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# download single file
|
||||||
|
GET http://localhost:5050/downloads
|
||||||
|
|
||||||
|
# download directory as archive
|
||||||
|
GET
|
||||||
|
|
||||||
|
# download single file using plugins
|
||||||
|
|
||||||
|
# download directory as archive using plugins
|
||||||
|
|
||||||
|
|
||||||
8
tests/02-upload-localhost.hurl
Normal file
8
tests/02-upload-localhost.hurl
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# upload a single new file
|
||||||
|
POST http://localhost:5050/upload
|
||||||
|
|
||||||
|
# upload a new directory
|
||||||
|
|
||||||
|
# upload a new plugin
|
||||||
|
|
||||||
|
# upload a new profile
|
||||||
2
tests/03-list-localhost.hurl
Normal file
2
tests/03-list-localhost.hurl
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
GET http://localhost:5050/list
|
||||||
|
GET http://localhost:5050/status
|
||||||
6
tests/04-profiles-localhost.hurl
Normal file
6
tests/04-profiles-localhost.hurl
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
GET http://localhost:5050/profiles
|
||||||
|
GET http://localhost:5050/profiles/test
|
||||||
|
POST http://localhost:5050/profiles/test
|
||||||
|
GET http://localhost:5050/profiles/test/data
|
||||||
|
POST http://localhost:5050/profiles/test/data
|
||||||
|
DELETE http://localhost:5050/profiles/test/data
|
||||||
6
tests/05-plugins-localhost.hurl
Normal file
6
tests/05-plugins-localhost.hurl
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
GET http://localhost:5050/plugins
|
||||||
|
GET http://localhost:5050/plugin/test
|
||||||
|
POST http://localhost:5050/plugins/test
|
||||||
|
DELETE http://localhost:5050/plugin/test
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,418 +0,0 @@
|
||||||
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{}
|
|
||||||
|
|
||||||
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 created for running tests."
|
|
||||||
}
|
|
||||||
func (g *TestGenerator) Generate(config *config.Config, params generator.Params) (generator.FileMap, error) {
|
|
||||||
// Jinja 2 template file
|
|
||||||
files := map[string]generator.Template{
|
|
||||||
"test1": generator.Template{
|
|
||||||
Contents: []byte(`
|
|
||||||
Name: {{plugin_name}}
|
|
||||||
Version: {{plugin_version}}
|
|
||||||
Description: {{plugin_description}}
|
|
||||||
|
|
||||||
This is the first test template file.
|
|
||||||
`),
|
|
||||||
},
|
|
||||||
"test2": generator.Template{
|
|
||||||
Contents: []byte(`
|
|
||||||
This is another testing Jinja 2 template file using {{plugin_name}}.
|
|
||||||
`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply Jinja templates to file
|
|
||||||
fileMap, err := generator.ApplyTemplates(generator.Mappings{
|
|
||||||
"plugin_name": g.GetName(),
|
|
||||||
"plugin_version": g.GetVersion(),
|
|
||||||
"plugin_description": g.GetDescription(),
|
|
||||||
}, files)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to apply templates: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure we have a valid config we can access
|
|
||||||
if config == nil {
|
|
||||||
return nil, fmt.Errorf("invalid config (config 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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.FileMap{"test": []byte("test")}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var Generator TestGenerator`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// get directory to replace remote pkg with local
|
|
||||||
// _, filename, _, _ := runtime.Caller(0)
|
|
||||||
// replaceDir := fmt.Sprintf("%s", filepath.Dir(workDir))
|
|
||||||
|
|
||||||
// show all paths to make sure we're using the correct ones
|
|
||||||
fmt.Printf("(TestPlugin) working directory: %v\n", workDir)
|
|
||||||
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)
|
|
||||||
|
|
||||||
// make temporary directory to test plugin
|
|
||||||
err = os.MkdirAll(testPluginDir, 0o777)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to make temporary directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// dump the plugin source code to a file
|
|
||||||
err = os.WriteFile(testPluginSourcePath, testPluginSource, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to write test plugin file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the source file was actually written
|
|
||||||
fileInfo, err := os.Stat(testPluginSourcePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to stat path: %v", err)
|
|
||||||
}
|
|
||||||
if fileInfo.IsDir() {
|
|
||||||
t.Fatalf("expected file but found directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
// change to testing directory to run command
|
|
||||||
err = os.Chdir(testPluginDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to 'cd' to temporary directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the plugin directory as a Go project
|
|
||||||
cmd := exec.Command("bash", "-c", "go mod init github.com/OpenCHAMI/configurator-test-plugin")
|
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
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 {
|
|
||||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute command to build the plugin
|
|
||||||
cmd = exec.Command("bash", "-c", "go build -buildmode=plugin -o=test-plugin.so test-plugin.go")
|
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
// stat the file to confirm that the plugin was built
|
|
||||||
fileInfo, err = os.Stat(testPluginPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to stat plugin file: %v", err)
|
|
||||||
}
|
|
||||||
if fileInfo.IsDir() {
|
|
||||||
t.Fatalf("directory file but a file was expected")
|
|
||||||
}
|
|
||||||
if fileInfo.Size() <= 0 {
|
|
||||||
t.Fatal("found an empty file or file with size of 0 bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// test loading plugins both individually and in a dir
|
|
||||||
gen, err := generator.LoadPlugin("test-plugin.so")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to load the test plugin: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test that we have all expected methods with type assertions
|
|
||||||
if _, ok := gen.(interface {
|
|
||||||
GetName() string
|
|
||||||
GetVersion() string
|
|
||||||
GetDescription() string
|
|
||||||
Generate(*config.Config, generator.Params) (generator.FileMap, error)
|
|
||||||
}); !ok {
|
|
||||||
t.Error("plugin does not implement all of the generator interface")
|
|
||||||
}
|
|
||||||
|
|
||||||
// test loading plugins from a directory (should just load a single one)
|
|
||||||
gens, err := generator.LoadPlugins(testPluginDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to load plugins in '%s': %v", testPluginDir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test all of the plugins loaded from a directory (should expect same result as above)
|
|
||||||
for _, gen := range gens {
|
|
||||||
if _, ok := gen.(interface {
|
|
||||||
GetName() string
|
|
||||||
GetVersion() string
|
|
||||||
GetDescription() string
|
|
||||||
Generate(*config.Config, generator.Params) (generator.FileMap, error)
|
|
||||||
}); !ok {
|
|
||||||
t.Error("plugin does not implement all of the generator interface")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that expects to fail with a specific error using a partially
|
|
||||||
// implemented generator. The purpose of this test is to make sure we're
|
|
||||||
// seeing the correct error that we would expect in these situations.
|
|
||||||
// The errors should be something like:
|
|
||||||
// - no symbol: "failed to look up symbol at path"
|
|
||||||
// - invalid symbol: "failed to load the correct symbol type at path"
|
|
||||||
func TestPluginWithInvalidOrNoSymbol(t *testing.T) {
|
|
||||||
var (
|
|
||||||
testPluginDir = t.TempDir()
|
|
||||||
testPluginPath = fmt.Sprintf("%s/invalid-plugin.so", testPluginDir)
|
|
||||||
testPluginSourcePath = fmt.Sprintf("%s/invalid-plugin.go", testPluginDir)
|
|
||||||
testPluginSource = []byte(`
|
|
||||||
package main
|
|
||||||
|
|
||||||
// An invalid generator that does not or partially implements
|
|
||||||
// the "Generator" interface.
|
|
||||||
type InvalidGenerator struct{}
|
|
||||||
func (g *InvalidGenerator) GetName() string { return "invalid" }
|
|
||||||
var Generator InvalidGenerator
|
|
||||||
`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// show all paths to make sure we're using the correct ones
|
|
||||||
fmt.Printf("(TestPluginWithInvalidOrNoSymbol) working directory: %v\n", workDir)
|
|
||||||
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 {
|
|
||||||
t.Fatalf("failed to make temporary directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// dump the plugin source code to a file
|
|
||||||
err = os.WriteFile(testPluginSourcePath, testPluginSource, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to write test plugin file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the source file was actually written
|
|
||||||
fileInfo, err := os.Stat(testPluginSourcePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to stat path: %v", err)
|
|
||||||
}
|
|
||||||
if fileInfo.IsDir() {
|
|
||||||
t.Fatalf("expected file but found directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
// change to testing directory to run command
|
|
||||||
err = os.Chdir(testPluginDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to 'cd' to temporary directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the plugin directory as a Go project
|
|
||||||
cmd := exec.Command("bash", "-c", "go mod init github.com/OpenCHAMI/configurator-test-plugin")
|
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
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 {
|
|
||||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute command to build the plugin
|
|
||||||
cmd = exec.Command("bash", "-c", "go build -buildmode=plugin -o=invalid-plugin.so invalid-plugin.go")
|
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
// stat the file to confirm that the plugin was built
|
|
||||||
fileInfo, err = os.Stat(testPluginPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to stat plugin file: %v", err)
|
|
||||||
}
|
|
||||||
if fileInfo.IsDir() {
|
|
||||||
t.Fatalf("directory file but a file was expected")
|
|
||||||
}
|
|
||||||
if fileInfo.Size() <= 0 {
|
|
||||||
t.Fatal("found an empty file or file with size of 0 bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// try and load plugin, but expect specific error
|
|
||||||
_, err = generator.LoadPlugin(testPluginSourcePath)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected an error, but returned nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that expects to successfully "generate" a file using the built-in
|
|
||||||
// example plugin with no fetching.
|
|
||||||
//
|
|
||||||
// NOTE: Normally we would dynamically load a generator from a plugin, but
|
|
||||||
// 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{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// make sure our generator returns expected strings
|
|
||||||
t.Run("properties", func(t *testing.T) {
|
|
||||||
if gen.GetName() != "test" {
|
|
||||||
t.Error("test generator return unexpected name")
|
|
||||||
}
|
|
||||||
if gen.GetVersion() != "v1.0.0" {
|
|
||||||
t.Error("test generator return unexpected version")
|
|
||||||
}
|
|
||||||
if gen.GetDescription() != "This is a plugin created for running tests." {
|
|
||||||
t.Error("test generator return unexpected description")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// try to generate a file with templating applied
|
|
||||||
fileMap, err := gen.Generate(&conf, generator.Params{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to generate file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test for 2 expected files to be generated in the output (hint: check the
|
|
||||||
// TestGenerator.Generate implementation)
|
|
||||||
if len(fileMap) != 2 {
|
|
||||||
t.Error("expected 2 files in generated output")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that expects to successfully "generate" a file using the built-in
|
|
||||||
// example plugin but by making a HTTP request to a service instance instead.
|
|
||||||
//
|
|
||||||
// NOTE: This test uses the default server settings to run. Also, no need to
|
|
||||||
// try and load the plugin from a lib here either.
|
|
||||||
func TestGenerateExampleWithServer(t *testing.T) {
|
|
||||||
var (
|
|
||||||
conf = config.New()
|
|
||||||
gen = TestGenerator{}
|
|
||||||
headers = make(map[string]string, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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{
|
|
||||||
TemplatePaths: []string{},
|
|
||||||
FilePaths: []string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new server, add test generator, and start in background
|
|
||||||
server := server.New(&conf)
|
|
||||||
generator.DefaultGenerators["test"] = &gen
|
|
||||||
go server.Serve()
|
|
||||||
|
|
||||||
// make request to server to generate a file
|
|
||||||
res, b, err := util.MakeRequest("http://127.0.0.1:3334/generate?target=test", http.MethodGet, nil, headers)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to make request: %v", err)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
t.Fatalf("expect status code 200 from response but received %d instead", res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test for specific output from request
|
|
||||||
//
|
|
||||||
// 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{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to generate file: %v", err)
|
|
||||||
}
|
|
||||||
for path, contents := range fileMap {
|
|
||||||
tmp := make(map[string]string, 1)
|
|
||||||
err := json.Unmarshal(b, &tmp)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to unmarshal response: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if string(contents) != string(tmp[path]) {
|
|
||||||
t.Fatalf("response does not match expected output...\nexpected:%s\noutput:%s", string(contents), string(tmp[path]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue