Skip to content

Commit

Permalink
Add a hack frontend for rootio action (#207)
Browse files Browse the repository at this point in the history
The rootio action is dependent on the Hegel `/metadata` endpoint; it requires storage device metadata only. This PR adds the `/metadata` endpoint back to Hegel temporarily until we migrate to exposing hardware data during Tinkerbell Template rendering where the rootio action will be passed its data as parameters.

The code lives in a new `hack` frontend to make clear its a hacky thing and with commentary on when it should be deleted.

The `/metadata` endpoint was tested by launching a K3D cluster and then launching Hegel manually. A Hardware was submitted to the cluster and the `/metadata` endpoint curled.

Relates to #188
  • Loading branch information
mergify[bot] authored and chrisdoherty4 committed Dec 9, 2022
1 parent 641a6b5 commit 0b02fda
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 24 deletions.
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ require (
sigs.k8s.io/controller-runtime v0.13.1
)

require github.com/kr/pretty v0.3.1 // indirect

require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
Expand Down Expand Up @@ -61,6 +59,7 @@ require (
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
Expand Down
2 changes: 2 additions & 0 deletions internal/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/tinkerbell/hegel/internal/backend/flatfile"
"github.com/tinkerbell/hegel/internal/backend/kubernetes"
"github.com/tinkerbell/hegel/internal/frontend/ec2"
"github.com/tinkerbell/hegel/internal/frontend/hack"
"github.com/tinkerbell/hegel/internal/healthcheck"
)

Expand All @@ -21,6 +22,7 @@ var ErrMultipleBackends = errors.New("only one backend option can be specified")
// this interface.
type Client interface {
ec2.Client
hack.Client
healthcheck.Client
}

Expand Down
13 changes: 13 additions & 0 deletions internal/backend/flatfile/hack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package flatfile

import (
"context"
"errors"

"github.com/tinkerbell/hegel/internal/frontend/hack"
)

// GetHackInstance exists to satisfy the hack.Client interface. It is not implemented.
func (b *Backend) GetHackInstance(context.Context, string) (hack.Instance, error) {
return hack.Instance{}, errors.New("unsupported")
}
30 changes: 18 additions & 12 deletions internal/backend/kubernetes/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/tinkerbell/hegel/internal/frontend/ec2"
tinkv1 "github.com/tinkerbell/tink/pkg/apis/core/v1alpha1"
tinkcontrollers "github.com/tinkerbell/tink/pkg/controllers"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -30,30 +29,28 @@ type Backend struct {
// between the cluster and internal caches. Consumers can wait for the initial sync using WaitForCachesync().
// See k8s.io/Backend-go/tools/Backendcmd for constructing *rest.Config objects.
func NewBackend(cfg BackendConfig) (*Backend, error) {
opts := tinkcontrollers.GetServerOptions()
opts.Namespace = cfg.Namespace

// Default the context.
if cfg.Context == nil {
cfg.Context = context.Background()
}

clientConfig := cfg.ClientConfig

// If no client was specified, build one and configure the backend with it including waiting
// for the caches to sync.
if cfg.ClientConfig == nil {
restConfig, err := loadConfig(cfg)
var err error
cfg, err = loadConfig(cfg)
if err != nil {
return nil, err
}
clientConfig = restConfig
}

opts := tinkcontrollers.GetServerOptions()
opts.Namespace = cfg.Namespace

// Use a manager from the tink project so we can take advantage of the indexes and caching it
// configures. Once started, we don't really need any of the manager capabilities hence we don't
// store it in the Backend.
manager, err := tinkcontrollers.NewManager(clientConfig, opts)
manager, err := tinkcontrollers.NewManager(cfg.ClientConfig, opts)
if err != nil {
return nil, err
}
Expand All @@ -73,7 +70,7 @@ func NewBackend(cfg BackendConfig) (*Backend, error) {
}, nil
}

func loadConfig(cfg BackendConfig) (*rest.Config, error) {
func loadConfig(cfg BackendConfig) (BackendConfig, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = cfg.Kubeconfig

Expand All @@ -89,10 +86,19 @@ func loadConfig(cfg BackendConfig) (*rest.Config, error) {
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
config, err := loader.ClientConfig()
if err != nil {
return nil, err
return BackendConfig{}, err
}
cfg.ClientConfig = config

// In the event no namespace was provided for override, we need to fill it in with whatever
// namespace was loaded from the kubeconfig.
namespace, _, err := loader.Namespace()
if err != nil {
return BackendConfig{}, err
}
cfg.Namespace = namespace

return config, nil
return cfg, nil
}

// IsHealthy returns true until the context used to create the Backend is cancelled.
Expand Down
43 changes: 40 additions & 3 deletions internal/backend/kubernetes/backend_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func TestBackend(t *testing.T) {

const ip = "10.10.10.10"
const hostname = "foobar"
const device = "/dev/sda"

hw := tinkv1.Hardware{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -71,6 +72,33 @@ func TestBackend(t *testing.T) {
Metadata: &tinkv1.HardwareMetadata{
Instance: &tinkv1.MetadataInstance{
Hostname: hostname,
Storage: &tinkv1.MetadataInstanceStorage{
Disks: []*tinkv1.MetadataInstanceStorageDisk{
{
Device: device,
WipeTable: true,
Partitions: []*tinkv1.MetadataInstanceStorageDiskPartition{
{
Label: "root",
Number: 0,
Size: 123,
},
},
},
},
Filesystems: []*tinkv1.MetadataInstanceStorageFilesystem{
{
Mount: &tinkv1.MetadataInstanceStorageMount{
Device: device,
Format: "ext4",
Create: &tinkv1.MetadataInstanceStorageMountFilesystemOptions{
Options: []string{"-L", "root"},
},
Point: "/",
},
},
},
},
},
},
},
Expand All @@ -90,12 +118,21 @@ func TestBackend(t *testing.T) {
}
backend.WaitForCacheSync(ctx)

instance, err := backend.GetEC2Instance(ctx, ip)
ec2instance, err := backend.GetEC2Instance(ctx, ip)
if err != nil {
t.Fatal(err)
}

if ec2instance.Metadata.Hostname != hostname {
t.Fatalf("Expected Hostname: %s; Received Hostname: %s\n", ec2instance.Metadata.Hostname, hostname)
}

hackInstance, err := backend.GetHackInstance(ctx, ip)
if err != nil {
t.Fatal(err)
}

if instance.Metadata.Hostname != hostname {
t.Fatalf("Expected Hostname: %s; Received Hostname: %s\n", instance.Metadata.Hostname, hostname)
if hackInstance.Metadata.Instance.Storage.Disks[0].Device != device {
t.Fatalf("Expected Device: %s; Received Device: %s\n", hackInstance.Metadata.Instance.Storage.Disks[0].Device, device)
}
}
36 changes: 36 additions & 0 deletions internal/backend/kubernetes/hack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package kubernetes

import (
"context"
"encoding/json"

"github.com/tinkerbell/hegel/internal/frontend/hack"
tinkv1 "github.com/tinkerbell/tink/pkg/apis/core/v1alpha1"
)

func (b *Backend) GetHackInstance(ctx context.Context, ip string) (hack.Instance, error) {
hw, err := b.retrieveByIP(ctx, ip)
if err != nil {
return hack.Instance{}, err
}

return toHackInstance(hw)
}

// toHackInstance converts a Tinkerbell Hardware resource to a hack.Instance by marshalling and
// unmarshalling. This works because the Hardware resource has historical roots that align with
// the hack.Instance struct that is derived from the rootio action. See the hack frontend for more
// details.
func toHackInstance(hw tinkv1.Hardware) (hack.Instance, error) {
marshalled, err := json.Marshal(hw.Spec)
if err != nil {
return hack.Instance{}, err
}

var i hack.Instance
if err := json.Unmarshal(marshalled, &i); err != nil {
return hack.Instance{}, err
}

return i, nil
}
22 changes: 17 additions & 5 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/spf13/viper"
"github.com/tinkerbell/hegel/internal/backend"
"github.com/tinkerbell/hegel/internal/frontend/ec2"
"github.com/tinkerbell/hegel/internal/frontend/hack"
hegelhttp "github.com/tinkerbell/hegel/internal/http"
"github.com/tinkerbell/hegel/internal/metrics"
"github.com/tinkerbell/hegel/internal/xff"
Expand Down Expand Up @@ -51,6 +52,9 @@ type RootCommandOptions struct {

FlatfilePath string `mapstructure:"flatfile-path"`

// Debug enables debug mode that maximizes logging.
Debug bool `mapstructure:"debug"`

// Hidden CLI flags.
HegelAPI bool `mapstructure:"hegel-api"`
}
Expand Down Expand Up @@ -100,6 +104,10 @@ func (c *RootCommand) Run(cmd *cobra.Command, _ []string) error {
}
defer logger.Close()

if !c.Opts.Debug {
gin.SetMode(gin.ReleaseMode)
}

logger.With("opts", fmt.Sprintf("%#v", c.Opts)).Info("Root command options")

ctx, otelShutdown := otelinit.InitOpenTelemetry(cmd.Context(), "hegel")
Expand All @@ -126,6 +134,8 @@ func (c *RootCommand) Run(cmd *cobra.Command, _ []string) error {
fe := ec2.New(be)
fe.Configure(router)

hack.Configure(router, be)

// Listen for signals to gracefully shutdown.
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer cancel()
Expand All @@ -134,6 +144,12 @@ func (c *RootCommand) Run(cmd *cobra.Command, _ []string) error {
}

func (c *RootCommand) configureFlags() error {
c.Flags().String(
"trusted-proxies",
"",
"A commma separated list of allowed peer IPs and/or CIDR blocks to replace with X-Forwarded-For",
)

c.Flags().Int("http-port", 50061, "Port to listen on for HTTP requests")

c.Flags().String("backend", "kubernetes", "Backend to use for metadata. Options: flatfile, kubernetes")
Expand All @@ -146,11 +162,7 @@ func (c *RootCommand) configureFlags() error {
// Flatfile backend specific flags.
c.Flags().String("flatfile-path", "", "Path to the flatfile metadata")

c.Flags().String(
"trusted-proxies",
"",
"A commma separated list of allowed peer IPs and/or CIDR blocks to replace with X-Forwarded-For",
)
c.Flags().Bool("debug", false, "Enable debug logging")

c.Flags().Bool("hegel-api", false, "Toggle to true to enable Hegel's new experimental API. Default is false.")
if err := c.Flags().MarkHidden("hegel-api"); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/tinkerbell/hegel/internal/cmd"
)

func TestHegel_EC2Frontend(t *testing.T) {
func TestHegel(t *testing.T) {
// Build the root command so we can launch it as if a main() func would.
root, err := cmd.NewRootCommand()
if err != nil {
Expand All @@ -39,7 +39,7 @@ func TestHegel_EC2Frontend(t *testing.T) {
// and begins listening. Slower machines may need a longer delay.
time.Sleep(50 * time.Millisecond)

t.Run("APIs", func(t *testing.T) {
t.Run("EC2", func(t *testing.T) {
// We have unit tests to validate the APIs serve correct data. These tests are to validate
// a static endpoint and a dynamic endpoint work as expected.
cases := []struct {
Expand Down
69 changes: 69 additions & 0 deletions internal/frontend/hack/hack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
Package hack contains a frontend that provides a /metadata endpoint for the rootio hub action.
It is not intended to be long lived and will be removed as we migrate to exposing Hardware
data to Tinkerbell templates. In doing so, we can convert the rootio action to accept its inputs
via parameters instead of retrieing them from Hegel and subsequently delete this frontend.
*/
package hack

import (
"context"
"errors"
"net/http"

"github.com/gin-gonic/gin"
"github.com/tinkerbell/hegel/internal/http/request"
)

// Client is a backend for retrieving hack instance data.
type Client interface {
GetHackInstance(ctx context.Context, ip string) (Instance, error)
}

// Instance is a representation of the instance metadata. Its based on the rooitio hub action
// and should have just enough information for it to work.
type Instance struct {
Metadata struct {
Instance struct {
Storage struct {
Disks []struct {
Device string `json:"device"`
Partitions []struct {
Label string `json:"label"`
Number int `json:"number"`
Size uint64 `json:"size"`
} `json:"partitions"`
WipeTable bool `json:"wipe_table"`
} `json:"disks"`
Filesystems []struct {
Mount struct {
Create struct {
Options []string `json:"options"`
} `json:"create"`
Device string `json:"device"`
Format string `json:"format"`
Point string `json:"point"`
} `json:"mount"`
} `json:"filesystems"`
} `json:"storage"`
} `json:"instance"`
} `json:"metadata"`
}

// Configure configures router with a `/metadata` endpoint using client to retrieve instance data.
func Configure(router gin.IRouter, client Client) {
router.GET("/metadata", func(ctx *gin.Context) {
ip, err := request.RemoteAddrIP(ctx.Request)
if err != nil {
_ = ctx.AbortWithError(http.StatusBadRequest, errors.New("invalid remote address"))
}

instance, err := client.GetHackInstance(ctx, ip)
if err != nil {
_ = ctx.AbortWithError(http.StatusInternalServerError, err)
return
}

ctx.JSON(200, instance)
})
}

0 comments on commit 0b02fda

Please sign in to comment.