blob: cceebd1dc5a6f84f54da5f2b3a3d1651eef7f13f [file] [log] [blame]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"time"
"source.monogon.dev/metropolis/cli/flagdefs"
metroctl "source.monogon.dev/metropolis/cli/metroctl/core"
"source.monogon.dev/metropolis/node"
cpb "source.monogon.dev/metropolis/proto/common"
mlaunch "source.monogon.dev/metropolis/test/launch"
)
const maxNodes = 256
func nodeSetFlag(p *[]int, name string, usage string) {
flag.Func(name, usage, func(val string) error {
for _, part := range strings.Split(val, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
startStr, endStr, ok := strings.Cut(part, "-")
if !ok {
endStr = startStr
}
start, err := strconv.Atoi(startStr)
if err != nil {
return err
}
end, err := strconv.Atoi(endStr)
if err != nil {
return err
}
if end >= maxNodes {
return fmt.Errorf("node index %v out of range, there can be at most %v nodes", end, maxNodes)
}
if end < start {
return fmt.Errorf("invalid range %q, end is smaller than start", part)
}
for i := start; i <= end; i++ {
*p = append(*p, i)
}
}
return nil
})
}
func sizeFlagMiB(p *int, name string, usage string) {
flag.Func(name, usage, func(val string) error {
multiplier := 1
switch {
case strings.HasSuffix(val, "M"):
case strings.HasSuffix(val, "G"):
multiplier = 1024
default:
return errors.New("must have suffix M for MiB or G for GiB")
}
intVal, err := strconv.Atoi(val[:len(val)-1])
if err != nil {
return err
}
*p = multiplier * intVal
return nil
})
}
func main() {
clusterConfig := cpb.ClusterConfiguration{}
opts := mlaunch.ClusterOptions{
NodeLogsToFiles: true,
InitialClusterConfiguration: &clusterConfig,
}
var consensusMemberList, kubernetesControllerList, kubernetesWorkerList []int
flag.IntVar(&opts.NumNodes, "num-nodes", 3, "Number of cluster nodes")
flagdefs.TPMModeVar(flag.CommandLine, &clusterConfig.TpmMode, "tpm-mode", cpb.ClusterConfiguration_TPM_MODE_REQUIRED, "TPM mode to set on cluster")
flagdefs.StorageSecurityPolicyVar(flag.CommandLine, &clusterConfig.StorageSecurityPolicy, "storage-security", cpb.ClusterConfiguration_STORAGE_SECURITY_POLICY_NEEDS_INSECURE, "Storage security policy to set on cluster")
flag.IntVar(&opts.Node.CPUs, "cpu", 1, "Number of virtual CPUs of each node")
flag.IntVar(&opts.Node.ThreadsPerCPU, "threads-per-cpu", 1, "Number of threads per CPU")
sizeFlagMiB(&opts.Node.MemoryMiB, "ram", "RAM size of each node, with suffix M for MiB or G for GiB")
nodeSetFlag(&consensusMemberList, "consensus-member", "List of nodes which get the Consensus Member role. Example: 0,3-5")
nodeSetFlag(&kubernetesControllerList, "kubernetes-controller", "List of nodes which get the Kubernetes Controller role. Example: 0,3-5")
nodeSetFlag(&kubernetesWorkerList, "kubernetes-worker", "List of nodes which get the Kubernetes Worker role. Example: 0,3-5")
flag.Parse()
if opts.NumNodes >= maxNodes {
log.Fatalf("num-nodes (%v) is too large, there can be at most %v nodes", opts.NumNodes, maxNodes)
}
for _, list := range [][]int{consensusMemberList, kubernetesControllerList, kubernetesWorkerList} {
for i := len(list) - 1; i >= 0; i-- {
if list[i] >= opts.NumNodes {
log.Fatalf("Node index %v out of range, can be at most %v", list[i], opts.NumNodes-1)
}
}
}
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
cl, err := mlaunch.LaunchCluster(ctx, opts)
if err != nil {
log.Fatalf("LaunchCluster: %v", err)
}
for _, node := range consensusMemberList {
cl.MakeConsensusMember(ctx, cl.NodeIDs[node])
}
for _, node := range kubernetesControllerList {
cl.MakeKubernetesController(ctx, cl.NodeIDs[node])
}
for _, node := range kubernetesWorkerList {
cl.MakeKubernetesWorker(ctx, cl.NodeIDs[node])
}
wpath, err := cl.MakeMetroctlWrapper()
if err != nil {
log.Fatalf("MakeWrapper: %v", err)
}
apiserver := cl.Nodes[cl.NodeIDs[0]].ManagementAddress
// Wait for the API server to start listening.
for {
conn, err := cl.DialNode(ctx, net.JoinHostPort(apiserver, node.KubernetesAPIWrappedPort.PortString()))
if err == nil {
conn.Close()
break
}
time.Sleep(100 * time.Millisecond)
}
// If the user has metroctl in their path, use the metroctl from path as
// a credential plugin. Otherwise use the path to the currently-running
// metroctl.
metroctlPath := "metroctl"
if _, err := exec.LookPath("metroctl"); err != nil {
metroctlPath, err = os.Executable()
if err != nil {
log.Fatalf("Failed to create kubectl entry as metroctl is neither in PATH nor can its absolute path be determined: %v", err)
}
}
configName := "launch-cluster"
if err := metroctl.InstallKubeletConfig(ctx, metroctlPath, cl.ConnectOptions(), configName, apiserver); err != nil {
log.Fatalf("InstallKubeletConfig: %v", err)
}
log.Printf("Launch: Cluster running!")
log.Printf(" To access cluster use: metroctl %s", cl.MetroctlFlags())
log.Printf(" Or use this handy wrapper: %s", wpath)
log.Printf(" To access Kubernetes, use kubectl --context=%s", configName)
<-ctx.Done()
cl.Close()
}