Skip to content
Open
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
95 changes: 85 additions & 10 deletions cmd/create/environment.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package create

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/dream11/odin/internal/service"
"github.com/dream11/odin/pkg/util"
environmentProto "github.com/dream11/odin/proto/gen/go/dream11/od/environment/v1"
providerAccountProto "github.com/dream11/odin/proto/gen/go/dream11/oam/provideraccount/v1"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/protobuf/encoding/protojson"
)

var envName string
var provisioningType string
var accounts string
var accountsFile string

var environmentClient service.Environment

Expand Down Expand Up @@ -42,9 +48,39 @@ func validateAccounts(accounts string) error {
return nil
}

func readAccountsFromFile(filePath string) ([]*providerAccountProto.GetProviderAccountResponse, error) {
// Read file
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read accounts file: %w", err)
}

// Parse JSON array
var accountsJSON []json.RawMessage
if err := json.Unmarshal(data, &accountsJSON); err != nil {
return nil, fmt.Errorf("failed to parse accounts file as JSON array: %w", err)
}

// Convert each JSON object to GetProviderAccountResponse
var accountResponses []*providerAccountProto.GetProviderAccountResponse
for i, accountJSON := range accountsJSON {
// Wrap in account field to match GetProviderAccountResponse structure
wrappedJSON := fmt.Sprintf(`{"account": %s}`, string(accountJSON))

var accountResp providerAccountProto.GetProviderAccountResponse
if err := protojson.Unmarshal([]byte(wrappedJSON), &accountResp); err != nil {
return nil, fmt.Errorf("failed to parse account at index %d: %w", i, err)
}
accountResponses = append(accountResponses, &accountResp)
}

return accountResponses, nil
}

func init() {
environmentCmd.Flags().StringVar(&envName, "name", "", "name of the environment to be created")
environmentCmd.Flags().StringVar(&accounts, "accounts", "", "list of comma separated cloud provider accounts")
environmentCmd.Flags().StringVar(&accountsFile, "accounts-file", "", "path to JSON file containing account details")
environmentCmd.Flags().StringVar(&provisioningType, "provisioning-type", "", "provisioning type of the environment")
err := environmentCmd.MarkFlagRequired("name")
if err != nil {
Expand All @@ -53,24 +89,63 @@ func init() {
if err := environmentCmd.MarkFlagRequired("provisioning-type"); err != nil {
log.Fatal("Error marking 'provisioning-type' flag as required:", err)
}
err = environmentCmd.MarkFlagRequired("accounts")
if err != nil {
log.Fatal("Error marking 'accounts' flag as required:", err)
}
createCmd.AddCommand(environmentCmd)
}

func execute(cmd *cobra.Command) {
ctx := cmd.Context()
// Validate accounts parameter
if err := validateAccounts(accounts); err != nil {
log.Fatal("Invalid accounts parameter: ", err)

// Validate mutual exclusivity of --accounts and --accounts-file
hasAccounts := accounts != ""
hasAccountsFile := accountsFile != ""

if hasAccounts && hasAccountsFile {
log.Fatal("Cannot use both --accounts and --accounts-file flags together")
}

// If neither flag provided, try default file
defaultAccountsFile := filepath.Join(os.Getenv("HOME"), ".odin", "local_account.json")
if !hasAccounts && !hasAccountsFile {
if _, err := os.Stat(defaultAccountsFile); err == nil {
log.Debugf("Using default accounts file: %s", defaultAccountsFile)
accountsFile = defaultAccountsFile
hasAccountsFile = true
} else {
log.Fatal("Either --accounts or --accounts-file must be provided, or create default file at ~/.odin/local_account.json")
}
}

// Auto-detect MAC address to use as routing key
macAddr, err := util.GetDefaultMACAddress()
if err != nil {
log.Fatalf("Failed to auto-detect MAC address: %v", err)
}
err := environmentClient.CreateEnvironment(&ctx, &environmentProto.CreateEnvironmentRequest{
log.Debugf("Using MAC address as routing key: %s", macAddr)

// Create the request
req := &environmentProto.CreateEnvironmentRequest{
EnvName: envName,
Accounts: util.SplitProviderAccount(accounts),
ProvisioningType: provisioningType,
})
RoutingKey: &macAddr,
}

// Populate accounts or account_details based on which flag was used
if hasAccounts {
// Existing behavior: validate and pass account names
if err := validateAccounts(accounts); err != nil {
log.Fatal("Invalid accounts parameter: ", err)
}
req.Accounts = util.SplitProviderAccount(accounts)
} else if hasAccountsFile {
// New behavior: read and pass full account data
accountDetails, err := readAccountsFromFile(accountsFile)
if err != nil {
log.Fatalf("Failed to read accounts file: %v", err)
}
req.AccountDetails = accountDetails
}

err = environmentClient.CreateEnvironment(&ctx, req)

if err != nil {
util.LogGrpcError(err, "Failed to create environment: ")
Expand Down
45 changes: 45 additions & 0 deletions pkg/util/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package util

import (
"fmt"
"net"
)

// GetDefaultMACAddress returns the MAC address of the en0 network interface.
// Falls back to the first non-loopback network interface if en0 is not available.
func GetDefaultMACAddress() (string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("failed to get network interfaces: %w", err)
}

// First, try to find en0 interface (matches install.sh behavior)
for _, iface := range interfaces {
if iface.Name == "en0" {
if len(iface.HardwareAddr) > 0 {
macAddr := iface.HardwareAddr.String()
if macAddr != "" && macAddr != "00:00:00:00:00:00" {
return macAddr, nil
}
}
}
}

// Fallback: find first non-loopback interface with valid MAC
for _, iface := range interfaces {
// Skip loopback, down interfaces, and interfaces without hardware address
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
continue
}

// Check if interface has a valid MAC address
if len(iface.HardwareAddr) > 0 {
macAddr := iface.HardwareAddr.String()
if macAddr != "" && macAddr != "00:00:00:00:00:00" {
return macAddr, nil
}
}
}

return "", fmt.Errorf("no suitable network interface with valid MAC address found")
}
3 changes: 3 additions & 0 deletions proto/dream11/od/environment/v1/environment.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ syntax = "proto3";
package dream11.od.environment.v1;

import "dream11/od/dto/v1/environment.proto";
import "dream11/oam/provideraccount/v1/provider_account.proto";
import "google/protobuf/struct.proto";

option go_package = "github.com/dream11/odin/proto/gen/go/dream11/od/environment/v1";
Expand Down Expand Up @@ -69,6 +70,8 @@ message CreateEnvironmentRequest {
string env_name = 1;
repeated string accounts = 2;
string provisioning_type = 3;
optional string routing_key = 4;
repeated dream11.oam.provideraccount.v1.GetProviderAccountResponse account_details = 5; // Full account data from file (bypasses OAM). Empty when using --accounts flag
}

message CreateEnvironmentResponse {
Expand Down
Loading
Loading