feat(agent)!: add support for mTLS authentication in gRPC server (#54)

* refactor(fancontroller): improve fan controller validation logic and error handling for temperature steps

* refactor(agent): restructure gRPC server implementation by moving it to a new api package for better organization and maintainability

* feat(agent): implement gRPC server for managing compute blade agents and add graceful shutdown support
refactor(agent): restructure agent code by moving API logic to a dedicated file and improving error handling
fix(agent): update logging messages for clarity and consistency across the agent's operations
chore(agent): remove unused API code and consolidate event handling logic for better maintainability
style(agent): improve code formatting and organization for better readability and adherence to conventions

* feat(agent): add support for TLS configuration in gRPC server

* feat(api): add gRPC server authentication

* fix

* feat(config): add listen mode configuration to support tcp or unix sockets
feat(agent): implement listen mode in gRPC service to allow flexible socket types
feat(bladectl): enhance configuration loading and add support for TLS credentials
fix(bladectl): improve error handling for gRPC connection and event emission
style(logging): change log level from Warn to Info for better clarity in logs

* add logging middleware + fixes

* fix remote-connection to gRPC API Server

debugging the SAN issues took the soul out of me... And then the stupid
mistake in cmd_root where I didn't construct the TLS credentials
correctly... Oh dear...

* cleanup

* cleanup

* cleanup commands

* cleanup

* make README.md nicer

* Update cmd/agent/main.go

Co-authored-by: Matthias Riegler <github@m4tbit.de>

* Update cmd/bladectl/cmd_root.go

Co-authored-by: Matthias Riegler <github@m4tbit.de>

* move bladectl config into correct directory

* fix bugs

* // FIXME: No dead code

* nit: code style

* nit(YAGNI): you aint gonna need it. Don't make life harder than it needs to be

* nit(YAGNI): you aint gonna need it. Don't make life harder than it needs to be

* nit(YAGNI): you aint gonna need it. Don't make life harder than it needs to be

* nit(cmd_identify)

---------

Co-authored-by: Matthias Riegler <github@m4tbit.de>
This commit is contained in:
Cedric Kienzler
2025-05-12 00:00:55 +02:00
committed by GitHub
parent ec6229ad86
commit 70541d86ba
60 changed files with 2189 additions and 650 deletions

View File

@@ -1,16 +1,15 @@
# Default configuration for the compute-blade-agent
log:
mode: production # production, development
# Listen configuration
listen:
metrics: ":9666"
grpc: /tmp/compute-blade-agent.sock
authenticated: false
mode: unix # tcp or unix
# Hardware abstraction layer configuration
hal:
# For the default fan unit, fanspeed measurement is causing a tiny bit of CPU laod.
# For the default fan unit, fanspeed measurement is causing a tiny bit of CPU load.
# Sometimes it might not be desired
rpm_reporting_standard_fan_unit: true
@@ -35,14 +34,13 @@ criticalLedColor:
# Enable/disable stealth mode; turns off all LEDs on the blade
stealth_mode: false
# Simple fan-speed controls based on the SoC temperature
fan_controller:
# For now, this is only supporting a two-step configuration.
steps:
- temperature: 45
percent: 40
- temperature: 55
percent: 80
# Critical temperature threshold
critical_temperature_threshold: 60

View File

@@ -2,8 +2,8 @@ package main
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/http/pprof"
"os"
@@ -14,12 +14,12 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/pflag"
"github.com/spf13/viper"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/uptime-induestries/compute-blade-agent/internal/agent"
"github.com/uptime-induestries/compute-blade-agent/pkg/log"
"github.com/uptime-industries/compute-blade-agent/internal/agent"
"github.com/uptime-industries/compute-blade-agent/internal/api"
"github.com/uptime-industries/compute-blade-agent/pkg/log"
"go.uber.org/zap"
"google.golang.org/grpc"
)
var (
@@ -28,13 +28,12 @@ var (
Date string
)
var debug = pflag.BoolP("debug", "v", false, "enable verbose logging")
func main() {
var wg sync.WaitGroup
pflag.Parse()
// Setup configuration
viper.SetConfigType("yaml")
// auto-bind environment variables
viper.SetEnvPrefix("BLADE")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
@@ -49,13 +48,11 @@ func main() {
// setup logger
var baseLogger *zap.Logger
switch logMode := viper.GetString("log.mode"); logMode {
case "development":
if debug != nil && *debug {
baseLogger = zap.Must(zap.NewDevelopment())
case "production":
} else {
baseLogger = zap.Must(zap.NewProduction())
default:
panic(fmt.Errorf("invalid log.mode: %s", logMode))
}
zapLogger := baseLogger.With(zap.String("app", "compute-blade-agent"))
@@ -71,73 +68,96 @@ func main() {
// load configuration
var cbAgentConfig agent.ComputeBladeAgentConfig
if err := viper.Unmarshal(&cbAgentConfig); err != nil {
log.FromContext(ctx).Error("Failed to load configuration", zap.Error(err))
cancelCtx(err)
log.FromContext(ctx).Fatal("Failed to load configuration", zap.Error(err))
}
// setup stop signal handlers
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
wg.Add(1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
defer wg.Done()
// Wait for context cancel or signal
select {
// Wait for context cancel
case <-ctx.Done():
// Wait for signal
case sig := <-sigs:
// On signal, cancel context
cancelCtx(fmt.Errorf("signal %s received", sig))
switch sig {
case syscall.SIGTERM:
fallthrough
case syscall.SIGINT:
fallthrough
case syscall.SIGQUIT:
// On terminate signal, cancel context causing the program to terminate
cancelCtx(fmt.Errorf("signal %s received", sig))
default:
log.FromContext(ctx).Warn("Received unknown signal", zap.String("signal", sig.String()))
}
}
}()
log.FromContext(ctx).Info("Bootstrapping compute-blade-agent", zap.String("version", Version), zap.String("commit", Commit), zap.String("date", Date))
computebladeAgent, err := agent.NewComputeBladeAgent(ctx, cbAgentConfig)
if err != nil {
log.FromContext(ctx).Error("Failed to create agent", zap.Error(err))
cancelCtx(err)
os.Exit(1)
log.FromContext(ctx).Fatal("Failed to create agent", zap.Error(err))
}
// Run agent
wg.Add(1)
go func() {
defer wg.Done()
log.FromContext(ctx).Info("Starting agent")
err := computebladeAgent.Run(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).Error("Failed to run agent", zap.Error(err))
cancelCtx(err)
}
}()
computebladeAgent.RunAsync(ctx, cancelCtx)
// Setup GRPC server
// FIXME add logging middleware
grpcServer := grpc.NewServer()
bladeapiv1alpha1.RegisterBladeAgentServiceServer(grpcServer, agent.NewGrpcServiceFor(computebladeAgent))
grpcServer := api.NewGrpcApiServer(ctx,
api.WithComputeBladeAgent(computebladeAgent),
api.WithAuthentication(cbAgentConfig.Listen.GrpcAuthenticated),
api.WithListenAddr(cbAgentConfig.Listen.Grpc),
api.WithListenMode(cbAgentConfig.Listen.GrpcListenMode),
)
// Run gRPC API
grpcServer.ServeAsync(ctx, cancelCtx)
// setup prometheus endpoint
promServer := runPrometheusEndpoint(ctx, cancelCtx, &cbAgentConfig.Listen)
// Wait for done
<-ctx.Done()
var wg sync.WaitGroup
// Shut-Down GRPC Server
wg.Add(1)
go func() {
defer wg.Done()
grpcListen, err := net.Listen("unix", viper.GetString("listen.grpc"))
if err != nil {
log.FromContext(ctx).Error("Failed to create grpc listener", zap.Error(err))
cancelCtx(err)
return
}
log.FromContext(ctx).Info("Starting grpc server", zap.String("address", viper.GetString("listen.grpc")))
if err := grpcServer.Serve(grpcListen); err != nil && err != grpc.ErrServerStopped {
log.FromContext(ctx).Error("Failed to start grpc server", zap.Error(err))
cancelCtx(err)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
log.FromContext(ctx).Info("Shutting down grpc server")
grpcServer.GracefulStop()
}()
// setup prometheus endpoint
// Shut-Down Prometheus Endpoint
wg.Add(1)
go func() {
defer wg.Done()
shutdownCtx, shutdownCtxCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCtxCancel()
if err := promServer.Shutdown(shutdownCtx); err != nil {
log.FromContext(ctx).Error("Failed to shutdown prometheus/pprof server", zap.Error(err))
}
}()
wg.Wait()
// Wait for context cancel
if err := ctx.Err(); !errors.Is(err, context.Canceled) {
log.FromContext(ctx).Fatal("Exiting", zap.Error(err))
} else {
log.FromContext(ctx).Info("Exiting")
}
}
func runPrometheusEndpoint(ctx context.Context, cancel context.CancelCauseFunc, apiConfig *api.Config) *http.Server {
instrumentationHandler := http.NewServeMux()
instrumentationHandler.Handle("/metrics", promhttp.Handler())
instrumentationHandler.HandleFunc("/debug/pprof/", pprof.Index)
@@ -145,33 +165,17 @@ func main() {
instrumentationHandler.HandleFunc("/debug/pprof/profile", pprof.Profile)
instrumentationHandler.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
instrumentationHandler.HandleFunc("/debug/pprof/trace", pprof.Trace)
server := &http.Server{Addr: ":9666", Handler: instrumentationHandler}
wg.Add(1)
server := &http.Server{Addr: apiConfig.Metrics, Handler: instrumentationHandler}
// Run Prometheus Endpoint
go func() {
defer wg.Done()
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.FromContext(ctx).Error("Failed to start prometheus/pprof server", zap.Error(err))
cancelCtx(err)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := server.Shutdown(shutdownCtx)
if err != nil {
log.FromContext(ctx).Error("Failed to shutdown prometheus/pprof server", zap.Error(err))
cancel(err)
}
}()
// Wait for context cancel
wg.Wait()
if err := ctx.Err(); err != nil && err != context.Canceled {
log.FromContext(ctx).Fatal("Exiting", zap.Error(err))
} else {
log.FromContext(ctx).Info("Exiting")
}
return server
}

View File

@@ -2,7 +2,7 @@ package main
import (
"github.com/spf13/cobra"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
bladeapiv1alpha1 "github.com/uptime-industries/compute-blade-agent/api/bladeapi/v1alpha1"
)
var (

View File

@@ -1,9 +1,11 @@
package main
import (
"fmt"
"github.com/sierrasoftworks/humane-errors-go"
"github.com/spf13/cobra"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
bladeapiv1alpha1 "github.com/uptime-industries/compute-blade-agent/api/bladeapi/v1alpha1"
"google.golang.org/protobuf/types/known/emptypb"
)
@@ -13,21 +15,27 @@ var (
)
func init() {
cmdIdentify.Flags().BoolVarP(&confirm, "confirm", "c", false, "confirm the identify state")
cmdIdentify.Flags().BoolVarP(&wait, "wait", "w", false, "Wait for the identify state to be confirmed (e.g. by a physical button press)")
cmdSet.AddCommand(cmdIdentify)
cmdSetIdentify.Flags().BoolVarP(&confirm, "confirm", "c", false, "confirm the identify state")
cmdSetIdentify.Flags().BoolVarP(&wait, "wait", "w", false, "Wait for the identify state to be confirmed (e.g. by a physical button press)")
cmdSet.AddCommand(cmdSetIdentify)
cmdRemove.AddCommand(cmdRmIdentify)
}
var cmdIdentify = &cobra.Command{
var cmdSetIdentify = &cobra.Command{
Use: "identify",
Example: "bladectl set identify --wait",
Short: "interact with the compute-blade identity LED",
RunE: runIdentity,
RunE: runSetIdentify,
}
func runIdentity(cmd *cobra.Command, _ []string) error {
var err error
var cmdRmIdentify = &cobra.Command{
Use: "identify",
Example: "bladectl unset identify",
Short: "remove the identify state with the compute-blade identity LED",
RunE: runRemoveIdentify,
}
func runSetIdentify(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client := clientFromContext(ctx)
@@ -38,17 +46,41 @@ func runIdentity(cmd *cobra.Command, _ []string) error {
}
// Emit the event to the compute-blade-agent
_, err = client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: event})
_, err := client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: event})
if err != nil {
return humane.Wrap(err, "failed to emit event", "ensure the compute-blade agent is running and responsive to requests", "check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'")
return fmt.Errorf(humane.Wrap(err,
"failed to emit event",
"ensure the compute-blade agent is running and responsive to requests",
"check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'",
).Display(),
)
}
// Check if we should wait for the identify state to be confirmed
if wait {
_, err := client.WaitForIdentifyConfirm(ctx, &emptypb.Empty{})
if err != nil {
return humane.Wrap(err, "unable to wait for confirmation", "ensure the compute-blade agent is running and responsive to requests", "check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'")
}
if !wait {
return nil
}
if _, err := client.WaitForIdentifyConfirm(ctx, &emptypb.Empty{}); err != nil {
return humane.Wrap(err, "unable to wait for confirmation", "ensure the compute-blade agent is running and responsive to requests", "check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'")
}
return nil
}
func runRemoveIdentify(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client := clientFromContext(ctx)
// Emit the event to the compute-blade-agent
_, err := client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: bladeapiv1alpha1.Event_IDENTIFY_CONFIRM})
if err != nil {
return fmt.Errorf(humane.Wrap(err,
"failed to emit event",
"ensure the compute-blade agent is running and responsive to requests",
"check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'",
).Display(),
)
}
return nil

View File

@@ -2,45 +2,161 @@ package main
import (
"context"
humane "github.com/sierrasoftworks/humane-errors-go"
"github.com/spf13/cobra"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"net"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/sierrasoftworks/humane-errors-go"
"github.com/spf13/cobra"
"github.com/spf13/viper"
bladeapiv1alpha1 "github.com/uptime-industries/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/uptime-industries/compute-blade-agent/cmd/bladectl/config"
"github.com/uptime-industries/compute-blade-agent/pkg/log"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
)
var (
bladeName string
timeout time.Duration
)
func init() {
rootCmd.PersistentFlags().StringVar(&bladeName, "blade", "", "Name of the compute-blade to control. If not provided, the compute-blade specified in `current-blade` will be used.")
rootCmd.PersistentFlags().DurationVar(&timeout, "timeout", time.Minute, "timeout for gRPC requests")
}
var rootCmd = &cobra.Command{
Use: "bladectl",
Short: "bladectl interacts with the compute-blade-agent and allows you to manage hardware-features of your compute blade(s)",
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
origCtx := cmd.Context()
// Load potential file configs
if err := viper.ReadInConfig(); err != nil {
return err
}
// load configuration
var bladectlCfg config.BladectlConfig
if err := viper.Unmarshal(&bladectlCfg); err != nil {
return err
}
var blade *config.Blade
blade, herr := bladectlCfg.FindBlade(bladeName)
if herr != nil {
return fmt.Errorf(herr.Display())
}
// setup signal handlers for SIGINT and SIGTERM
ctx, cancelCtx := context.WithTimeout(origCtx, timeout)
// setup signal handler channels
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
// Wait for context cancel or signal
select {
// Wait for context cancel
case <-ctx.Done():
case <-sigs:
// On signal, cancel context
cancelCtx()
// Wait for signal
case sig := <-sigs:
switch sig {
case syscall.SIGTERM:
fallthrough
case syscall.SIGINT:
fallthrough
case syscall.SIGQUIT:
// On terminate signal, cancel context causing the program to terminate
cancelCtx()
default:
log.FromContext(ctx).Warn("Received unknown signal", zap.String("signal", sig.String()))
}
}
}()
conn, err := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return humane.Wrap(err, "failed to dial grpc server", "ensure the gRPC server you are trying to connect to is running and the address is correct")
}
client := bladeapiv1alpha1.NewBladeAgentServiceClient(conn)
// Create our gRPC Transport Credentials
credentials := insecure.NewCredentials()
certData := blade.Certificate
// If we're presented with certificate data in the config, we try to create a mTLS connection
if len(certData.ClientCertificateData) > 0 && len(certData.ClientKeyData) > 0 && len(certData.CertificateAuthorityData) > 0 {
var err error
serverName := blade.Server
if strings.Contains(serverName, ":") {
if serverName, _, err = net.SplitHostPort(blade.Server); err != nil {
return fmt.Errorf("failed to parse server address: %w", err)
}
}
if credentials, err = loadTlsCredentials(serverName, certData); err != nil {
return err
}
}
conn, err := grpc.NewClient(blade.Server, grpc.WithTransportCredentials(credentials))
if err != nil {
return fmt.Errorf(
humane.Wrap(err,
"failed to dial grpc server",
"ensure the gRPC server you are trying to connect to is running and the address is correct",
).Display(),
)
}
client := bladeapiv1alpha1.NewBladeAgentServiceClient(conn)
cmd.SetContext(clientIntoContext(ctx, client))
return nil
},
}
func loadTlsCredentials(server string, certData config.Certificate) (credentials.TransportCredentials, error) {
// Decode base64 certificate, key, and CA
certPEM, err := base64.StdEncoding.DecodeString(certData.ClientCertificateData)
if err != nil {
return nil, fmt.Errorf("invalid base64 client cert: %w", err)
}
keyPEM, err := base64.StdEncoding.DecodeString(certData.ClientKeyData)
if err != nil {
return nil, fmt.Errorf("invalid base64 client key: %w", err)
}
caPEM, err := base64.StdEncoding.DecodeString(certData.CertificateAuthorityData)
if err != nil {
return nil, fmt.Errorf("invalid base64 CA cert: %w", err)
}
// Load client cert/key pair
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, fmt.Errorf("failed to parse client cert/key pair: %w", err)
}
// Load CA into CertPool
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(caPEM) {
return nil, fmt.Errorf("failed to append CA certificate")
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{tlsCert},
RootCAs: caPool,
ServerName: server,
}
return credentials.NewTLS(tlsConfig), nil
}

View File

@@ -21,4 +21,11 @@ var (
Short: "Configure compute-blade",
Long: "These commands allow you make changes to compute-blade related information.",
}
cmdRemove = &cobra.Command{
Use: "remove",
Aliases: []string{"rm", "delete", "del", "unset"},
Short: "Configure compute-blade",
Long: "These commands allow you make changes to compute-blade related information.",
}
)

View File

@@ -2,6 +2,7 @@ package main
import (
"fmt"
"github.com/spf13/cobra"
)

View File

@@ -0,0 +1,94 @@
package config
import (
"encoding/base64"
"os"
"path/filepath"
"github.com/sierrasoftworks/humane-errors-go"
"go.uber.org/zap"
)
type BladectlConfig struct {
Blades []NamedBlade `yaml:"blades" mapstructure:"blades"`
CurrentBlade string `yaml:"current-blade" mapstructure:"current-blade"`
}
type NamedBlade struct {
Name string `yaml:"name" mapstructure:"name"`
Blade Blade `yaml:"blade" mapstructure:"blade"`
}
type Blade struct {
Server string `yaml:"server" mapstructure:"server"`
Certificate Certificate `yaml:"cert,omitempty" mapstructure:"cert,omitempty"`
}
type Certificate struct {
CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty" mapstructure:"certificate-authority-data,omitempty"`
ClientCertificateData string `yaml:"client-certificate-data,omitempty" mapstructure:"client-certificate-data,omitempty"`
ClientKeyData string `yaml:"client-key-data,omitempty" mapstructure:"client-key-data,omitempty"`
}
func (c *BladectlConfig) FindBlade(name string) (*Blade, humane.Error) {
if len(name) == 0 {
name = c.CurrentBlade
}
for _, blade := range c.Blades {
if blade.Name == name {
return &blade.Blade, nil
}
}
return nil, humane.New("current blade not found in configuration",
"ensure you have a current-blade set in your configuration file, or use the --current-blade flag to specify one",
"make sure you have a blade with the name you specified in the blades configuration",
)
}
func NewAuthenticatedBladectlConfig(server string, caPEM []byte, clientCertDER []byte, clientKeyDER []byte) *BladectlConfig {
cfg := NewBladectlConfig(server)
cfg.Blades[0].Blade.Certificate.CertificateAuthorityData = base64.StdEncoding.EncodeToString(caPEM)
cfg.Blades[0].Blade.Certificate.ClientCertificateData = base64.StdEncoding.EncodeToString(clientCertDER)
cfg.Blades[0].Blade.Certificate.ClientKeyData = base64.StdEncoding.EncodeToString(clientKeyDER)
return cfg
}
func NewBladectlConfig(server string) *BladectlConfig {
hostname, err := os.Hostname()
if err != nil {
zap.L().Fatal("Failed to extract hostname", zap.Error(err))
}
return &BladectlConfig{
Blades: []NamedBlade{
{
Name: hostname,
Blade: Blade{
Server: server,
},
},
},
CurrentBlade: hostname,
}
}
func EnsureBladectlConfigHome() (string, humane.Error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", humane.Wrap(err, "Failed to extract home directory",
"this should never happen",
"please report this as a bug to https://github.com/uptime-industries/compute-blade-agent/issues",
)
}
configDir := filepath.Join(homeDir, ".config", "bladectl")
if err := os.MkdirAll(configDir, 0700); err != nil {
return "", humane.Wrap(err, "Failed to create config directory",
"ensure the home-directory is writable by the agent user",
)
}
return configDir, nil
}

View File

@@ -3,33 +3,24 @@ package main
import (
"context"
"log"
"time"
"strings"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/spf13/viper"
bladeapiv1alpha1 "github.com/uptime-industries/compute-blade-agent/api/bladeapi/v1alpha1"
)
type grpcClientContextKey int
const (
defaultGrpcClientContextKey grpcClientContextKey = 0
defaultGrpcClientConnContextKey grpcClientContextKey = 1
defaultGrpcClientContextKey grpcClientContextKey = 0
)
var (
grpcAddr string
timeout time.Duration
Version string
Commit string
Date string
)
func init() {
rootCmd.PersistentFlags().
StringVar(&grpcAddr, "addr", "unix:///tmp/compute-blade-agent.sock", "address of the compute-blade-agent gRPC server")
rootCmd.PersistentFlags().DurationVar(&timeout, "timeout", time.Minute, "timeout for gRPC requests")
}
func clientIntoContext(ctx context.Context, client bladeapiv1alpha1.BladeAgentServiceClient) context.Context {
return context.WithValue(ctx, defaultGrpcClientContextKey, client)
}
@@ -43,6 +34,13 @@ func clientFromContext(ctx context.Context) bladeapiv1alpha1.BladeAgentServiceCl
}
func main() {
// Setup configuration
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("$HOME/.config/bladectl")
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}

View File

@@ -4,14 +4,15 @@ package main
import (
"context"
"machine"
"time"
"github.com/uptime-induestries/compute-blade-agent/pkg/eventbus"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/emc2101"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/proto"
"machine"
"github.com/uptime-industries/compute-blade-agent/pkg/events"
"github.com/uptime-industries/compute-blade-agent/pkg/hal/led"
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit"
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit/emc2101"
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit/proto"
"golang.org/x/sync/errgroup"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/ws2812"
@@ -33,7 +34,7 @@ type Controller struct {
LeftUART drivers.UART
RightUART drivers.UART
eb eventbus.EventBus
eb events.EventBus
leftLed led.Color
rightLed led.Color
leftReqFanSpeed uint8
@@ -43,7 +44,7 @@ type Controller struct {
}
func (c *Controller) Run(parentCtx context.Context) error {
c.eb = eventbus.New()
c.eb = events.New()
c.FanController.Init()
c.FanController.SetFanPercent(c.DefaultFanSpeed)
@@ -80,7 +81,7 @@ func (c *Controller) Run(parentCtx context.Context) error {
})
// right blade events
println("[+] Starting event listener (righ)")
println("[+] Starting event listener (right)")
group.Go(func() error {
return c.listenEvents(ctx, c.RightUART, rightBladeTopicIn)
})
@@ -123,7 +124,7 @@ func (c *Controller) Run(parentCtx context.Context) error {
return group.Wait()
}
// listenEvents reads events from the UART interface and dispatches them to the eventbus
// listenEvents reads events from the UART interface and dispatches them to the events
func (c *Controller) listenEvents(ctx context.Context, uart drivers.UART, targetTopic string) error {
for {
// Read packet from UART; blocks until packet is received
@@ -137,9 +138,9 @@ func (c *Controller) listenEvents(ctx context.Context, uart drivers.UART, target
}
}
// dispatchEvents reads events from the eventbus and writes them to the UART interface
// dispatchEvents reads events from the events and writes them to the UART interface
func (c *Controller) dispatchEvents(ctx context.Context, uart drivers.UART, sourceTopic string) error {
sub := c.eb.Subscribe(sourceTopic, 4, eventbus.MatchAll)
sub := c.eb.Subscribe(sourceTopic, 4, events.MatchAll)
defer sub.Unsubscribe()
for {
select {
@@ -201,9 +202,9 @@ func (c *Controller) metricReporter(ctx context.Context) error {
func (c *Controller) updateFanSpeed(ctx context.Context) error {
var pkt smartfanunit.SetFanSpeedPercentPacket
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, eventbus.MatchAll)
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, events.MatchAll)
defer subLeft.Unsubscribe()
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, eventbus.MatchAll)
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, events.MatchAll)
defer subRight.Unsubscribe()
for {

View File

@@ -4,11 +4,12 @@ package main
import (
"context"
"machine"
"time"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/emc2101"
"machine"
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit"
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit/emc2101"
"tinygo.org/x/drivers/ws2812"
)
@@ -26,15 +27,15 @@ func main() {
err = machine.UART0.Configure(machine.UARTConfig{TX: machine.UART0_TX_PIN, RX: machine.UART0_RX_PIN})
if err != nil {
println("[!] Failed to initialize UART0:", err.Error())
goto errprint
goto errPrint
}
machine.UART0.SetBaudRate(smartfanunit.Baudrate)
machine.UART0.SetBaudRate(smartfanunit.BaudRate)
err = machine.UART1.Configure(machine.UARTConfig{TX: machine.UART1_TX_PIN, RX: machine.UART1_RX_PIN})
if err != nil {
println("[!] Failed to initialize UART1:", err.Error())
goto errprint
goto errPrint
}
machine.UART1.SetBaudRate(smartfanunit.Baudrate)
machine.UART1.SetBaudRate(smartfanunit.BaudRate)
// Enables fan, DO NOT CHANGE
machine.GP16.Configure(machine.PinConfig{Mode: machine.PinOutput})
@@ -57,7 +58,7 @@ func main() {
err = emc.Init()
if err != nil {
println("[!] Failed to initialize emc2101:", err.Error())
goto errprint
goto errPrint
}
println("[+] IO initialized, starting controller...")
@@ -75,7 +76,7 @@ func main() {
err = controller.Run(context.Background())
// Blinking -> something went wrong
errprint:
errPrint:
ledState := false
for {
ledState = !ledState