Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/sfcompute/nodes-go v0.1.0-alpha.4
github.com/sfcompute/sfc-go v0.1.0-preview.3
github.com/stretchr/testify v1.11.1
github.com/verda-cloud/verdacloud-sdk-go v1.4.2
golang.org/x/crypto v0.52.0
golang.org/x/text v0.37.0
google.golang.org/grpc v1.79.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/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=
github.com/verda-cloud/verdacloud-sdk-go v1.4.2 h1:oVb8fHVQOY+YPuuMYMee9gYCkPTwAw01LmkqxM21T/Y=
github.com/verda-cloud/verdacloud-sdk-go v1.4.2/go.mod h1:pmlpiCL9fTSikZ3qWLJPpHOG0E8PKkQVUX5s4Z+SktY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
137 changes: 137 additions & 0 deletions v1/providers/verda/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package verda

import (
"fmt"
"net"
"strings"

v1 "github.com/brevdev/cloud/v1"
)

const (
dockerFirewallScriptPath = "/usr/local/sbin/brev-apply-docker-firewall.sh"
dockerFirewallDropInPath = "/etc/systemd/system/docker.service.d/10-brev-firewall.conf"
)

func buildStartupScript(rules v1.FirewallRules) (string, error) {
ufwRules, dockerRules, err := firewallRuleCommands(rules.IngressRules)
if err != nil {
return "", err
}

var script strings.Builder
script.WriteString(`#!/bin/bash
set -u

if ! command -v ufw >/dev/null 2>&1; then
apt-get update -y
DEBIAN_FRONTEND=noninteractive apt-get install -y ufw iptables
fi

mkdir -p /usr/local/sbin /etc/systemd/system/docker.service.d
cat > ` + dockerFirewallScriptPath + ` <<'BREV_FIREWALL'
#!/bin/sh
iptables -N DOCKER-USER 2>/dev/null || true
iptables -F DOCKER-USER || true
iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A DOCKER-USER -i docker0 ! -o docker0 -j ACCEPT
iptables -A DOCKER-USER -i br+ ! -o br+ -j ACCEPT
iptables -A DOCKER-USER -i cni+ ! -o cni+ -j ACCEPT
iptables -A DOCKER-USER -i cali+ ! -o cali+ -j ACCEPT
iptables -A DOCKER-USER -i docker0 -o docker0 -j ACCEPT
iptables -A DOCKER-USER -i br+ -o br+ -j ACCEPT
iptables -A DOCKER-USER -i cni+ -o cni+ -j ACCEPT
iptables -A DOCKER-USER -i cali+ -o cali+ -j ACCEPT
iptables -A DOCKER-USER -i lo -j ACCEPT
iptables -A DOCKER-USER -i wt0 -j ACCEPT
`)
for _, command := range dockerRules {
script.WriteString(command)
script.WriteByte('\n')
}
script.WriteString(`iptables -A DOCKER-USER -j DROP
exit 0
BREV_FIREWALL
chmod 0755 ` + dockerFirewallScriptPath + `

cat > ` + dockerFirewallDropInPath + ` <<'BREV_DROP_IN'
[Service]
ExecStartPost=-` + dockerFirewallScriptPath + `
BREV_DROP_IN

systemctl daemon-reload || true
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
`)
for _, command := range ufwRules {
script.WriteString(command)
script.WriteByte('\n')
}
script.WriteString(`ufw --force enable
` + dockerFirewallScriptPath + ` || true
`)
return script.String(), nil
}

func firewallRuleCommands(rules []v1.FirewallRule) ([]string, []string, error) {
var ufwCommands []string
var dockerCommands []string
for _, rule := range rules {
if rule.FromPort < 1 || rule.ToPort > 65535 || rule.FromPort > rule.ToPort {
return nil, nil, fmt.Errorf(
"invalid firewall port range %d-%d",
rule.FromPort,
rule.ToPort,
)
}

sources := rule.IPRanges
if len(sources) == 0 {
sources = []string{"0.0.0.0/0"}
}
for _, source := range sources {
ip, network, err := net.ParseCIDR(source)
if err != nil {
return nil, nil, fmt.Errorf("invalid firewall CIDR %q: %w", source, err)
}
if ip.To4() == nil {
return nil, nil, fmt.Errorf("IPv6 firewall CIDR %q is not supported", source)
}
source = network.String()

if rule.FromPort == rule.ToPort {
ufwCommands = append(ufwCommands, fmt.Sprintf(
"ufw allow from %s to any port %d",
source,
rule.FromPort,
))
} else {
for _, protocol := range []string{"tcp", "udp"} {
ufwCommands = append(ufwCommands, fmt.Sprintf(
"ufw allow from %s to any port %d:%d proto %s",
source,
rule.FromPort,
rule.ToPort,
protocol,
))
}
}

portSpec := fmt.Sprintf("%d", rule.FromPort)
if rule.FromPort != rule.ToPort {
portSpec = fmt.Sprintf("%d:%d", rule.FromPort, rule.ToPort)
}
for _, protocol := range []string{"tcp", "udp"} {
dockerCommands = append(dockerCommands, fmt.Sprintf(
"iptables -A DOCKER-USER -s %s -p %s --dport %s -j ACCEPT",
source,
protocol,
portSpec,
))
}
}
}
return ufwCommands, dockerCommands, nil
}
20 changes: 20 additions & 0 deletions v1/providers/verda/capabilities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package verda

import (
"context"

v1 "github.com/brevdev/cloud/v1"
)

func getCapabilities() v1.Capabilities {
return v1.Capabilities{
v1.CapabilityCreateInstance,
v1.CapabilityTerminateInstance,
v1.CapabilityCreateTerminateInstance,
v1.CapabilityStopStartInstance,
}
}

func (c *VerdaClient) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
return getCapabilities(), nil
}
165 changes: 165 additions & 0 deletions v1/providers/verda/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package verda

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/pkg/errors"

v1 "github.com/brevdev/cloud/v1"
validation "github.com/go-ozzo/ozzo-validation/v4"
verdago "github.com/verda-cloud/verdacloud-sdk-go/pkg/verda"
)

const (
CloudProviderID = "verda"
DefaultAPIURL = verdago.DefaultBaseURL
)

type VerdaCredential struct {
RefID string
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
APIURL string `json:"api_url"`
}

var _ v1.CloudCredential = &VerdaCredential{}

func NewVerdaCredential(refID, clientID, clientSecret string) *VerdaCredential {
credential := &VerdaCredential{
RefID: refID,
ClientID: clientID,
ClientSecret: clientSecret,
}
credential.SetDefaults()
return credential
}

func (c *VerdaCredential) SetDefaults() {
if c.APIURL == "" {
c.APIURL = DefaultAPIURL
}
c.APIURL = strings.TrimRight(c.APIURL, "/")
}

func (c *VerdaCredential) Validate() error {
c.SetDefaults()
if err := validation.ValidateStruct(
c,
validation.Field(&c.ClientID, validation.Required),
validation.Field(&c.ClientSecret, validation.Required),
validation.Field(&c.APIURL, validation.Required),
); err != nil {
return errors.Wrap(err, "failed to validate verda credential")
}
return nil
}

func (c *VerdaCredential) GetReferenceID() string {
return c.RefID
}

func (c *VerdaCredential) GetAPIType() v1.APIType {
return v1.APITypeGlobal
}

func (c *VerdaCredential) GetCloudProviderID() v1.CloudProviderID {
return CloudProviderID
}

func (c *VerdaCredential) GetTenantID() (string, error) {
return makeTenantID(c.ClientID)
}

func makeTenantID(clientID string) (string, error) {
hashedClientID, err := v1.HashSensitiveString(clientID)
if err != nil {
return "", errors.Wrap(err, "failed to hash verda client ID")
}
return fmt.Sprintf("%s-%s", CloudProviderID, hashedClientID), nil
}

func (c *VerdaCredential) MakeClient(ctx context.Context, location string) (v1.CloudClient, error) {
return c.MakeClientWithOptions(ctx, location)
}

func (c *VerdaCredential) MakeClientWithOptions(_ context.Context, location string, opts ...VerdaClientOption) (v1.CloudClient, error) {
return NewVerdaClient(*c, location, opts...)
}

func (c *VerdaCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
return getCapabilities(), nil
}

type VerdaClient struct {
v1.NotImplCloudClient

refID string
clientID string
location string
client *verdago.Client
httpClient *http.Client
}

var _ v1.CloudClient = &VerdaClient{}

type VerdaClientOption func(*VerdaClient)

func WithHTTPClient(httpClient *http.Client) VerdaClientOption {
return func(c *VerdaClient) {
c.httpClient = httpClient
}
}

func NewVerdaClient(credential VerdaCredential, location string, opts ...VerdaClientOption) (*VerdaClient, error) {
if err := credential.Validate(); err != nil {
return nil, err
}

client := &VerdaClient{
refID: credential.RefID,
clientID: credential.ClientID,
location: location,
httpClient: http.DefaultClient,
}
for _, opt := range opts {
opt(client)
}

sdkClient, err := verdago.NewClient(
verdago.WithBaseURL(credential.APIURL),
verdago.WithClientID(credential.ClientID),
verdago.WithClientSecret(credential.ClientSecret),
verdago.WithHTTPClient(client.httpClient),
verdago.WithUserAgent("brev-cloud"),
)
if err != nil {
return nil, errors.Wrap(err, "failed to create verda SDK client")
}
client.client = sdkClient
return client, nil
}

func (c *VerdaClient) GetReferenceID() string {
return c.refID
}

func (c *VerdaClient) GetAPIType() v1.APIType {
return v1.APITypeGlobal
}

func (c *VerdaClient) GetCloudProviderID() v1.CloudProviderID {
return CloudProviderID
}

func (c *VerdaClient) GetTenantID() (string, error) {
return makeTenantID(c.clientID)
}

func (c *VerdaClient) MakeClient(_ context.Context, location string) (v1.CloudClient, error) {
clientCopy := *c
clientCopy.location = location
return &clientCopy, nil
}
Loading
Loading