⚠⚠⚠ Deprecated: ⚠⚠⚠
This repository is deprecated in favor of libcontainerssh for ContainerSSH 0.5.
This library provides configuration client and server components for dynamic SSH configuration.
The main use case of this library will be creating a configuration server to match the current release of ContainerSSH in Go.
First, you need to fetch this library as a dependency using go modules:
go get github.com/containerssh/configuration
Next, you will have to write an implementation for the following interface:
type ConfigRequestHandler interface {
OnConfig(request configuration.ConfigRequest) (configuration.AppConfig, error)
}
The best way to do this is creating a struct and adding a method with a receiver:
type myConfigReqHandler struct {
}
func (m *myConfigReqHandler) OnConfig(
request configuration.ConfigRequest,
) (config configuration.AppConfig, err error) {
// We recommend using an IDE to discover the possible options here.
if request.Username == "foo" {
config.DockerRun.Config.ContainerConfig.Image = "yourcompany/yourimage"
}
return config, err
}
Warning! Your OnConfig
method should only return an error if it can genuinely not serve the request. This should not be used as a means to reject users. This should be done using the authentication server. If you return an error ContainerSSH will retry the request several times in an attempt to work around network failures.
Once you have your handler implemented you must decide which method you want to use for integration.
This method is useful if you don't want to run anything else on the webserver, only the config endpoint. You can create a new server like this:
srv, err := configuration.NewServer(
http.ServerConfiguration{
Listen: "0.0.0.0:8080",
},
&myConfigReqHandler{},
logger,
)
The logger
parameter is a logger from the ContainerSSH log library.
Once you have the server you can start it using the service library:
lifecycle := service.NewLifecycle(srv)
err := lifecycle.Run()
This will run your server in an endless fashion. However, for a well-behaved server you should also implement signal handling:
srv, err := configuration.NewServer(
http.ServerConfiguration{
Listen: "0.0.0.0:8080",
},
&myConfigReqHandler{},
logger,
)
if err != nil {
// Handle error
}
lifecycle := service.NewLifecycle(srv)
go func() {
//Ignore error, handled later.
_ = lifecycle.Run()
}()
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
go func() {
if _, ok := <-signals; ok {
// ok means the channel wasn't closed, let's trigger a shutdown.
lifecycle.Shutdown(
context.WithTimeout(
context.Background(),
20 * time.Second,
)
)
}
}()
// Wait for the service to terminate.
lastError := lifecycle.Wait()
// We are already shutting down, ignore further signals
signal.Ignore(syscall.SIGINT, syscall.SIGTERM)
// close signals channel so the signal handler gets terminated
close(signals)
if err != nil {
// Exit with a non-zero signal
fmt.Fprintf(
os.Stderr,
"an error happened while running the server (%v)",
err,
)
os.Exit(1)
}
os.Exit(0)
Note: We recommend securing client-server communication with certificates. The details about securing your HTTP requests are documented in the HTTP library.
Use this method if you want to integrate your handler with an existing Go HTTP server. This is rather simple:
handler, err := configuration.NewHandler(&myConfigReqHandler{}, logger)
You can now use the handler
variable as a handler for the http
package or a MUX like gorilla/mux.
This library also contains the components to call the configuration server in a simplified fashion. To create a client simply call the following method:
client, err := configuration.NewClient(
configuration.ClientConfig{
http.ClientConfiguration{
URL: "http://your-server/config-endpoint/"
}
},
logger,
metricsCollector,
)
The logger
is a logger from the log library, the metricsCollector
is supplied by the metrics library.
You can now use the client
variable to fetch the configuration specific to a connecting client:
connectionID := "0123456789ABCDEF"
appConfig, err := client.Get(
ctx,
"my-name-is-trinity",
net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 2222,
},
connectionID,
) (AppConfig, error)
Now you have the client-specific configuration in appConfig
.
Note: We recommend securing client-server communication with certificates. The details about securing your HTTP requests are documented in the HTTP library.
This library also provides simplified methods for reading the configuration from an io.Reader
and writing it to an io.Writer
.
file, err := os.Open("file.yaml")
// ...
loader, err := configuration.NewReaderLoader(
file,
logger,
configuration.FormatYAML,
)
// Read global config
appConfig := &configuration.AppConfig{}
err := loader.Load(ctx, appConfig)
// Read connection-specific config:
err := loader.LoadConnection(
ctx,
"my-name-is-trinity",
net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 2222,
},
connectionID,
appConfig,
)
As you can see these loaders are designed to be chained together. For example, you could add a HTTP loader after the file loader:
httpLoader, err := configuration.NewHTTPLoader(clientConfig, logger)
This HTTP loader calls the HTTP client described above.
Conversely, you can write the configuration to a YAML format:
saver, err := configuration.NewWriterSaver(
os.Stdout,
logger,
configuration.FormatYAML,
)
err := saver.Save(appConfig)