From 142416ee47afc3e1288e040814cc53e36b8381ce Mon Sep 17 00:00:00 2001 From: Clark McCauley Date: Sat, 25 Nov 2023 10:24:49 -0700 Subject: [PATCH] Just a lot of changes --- .goreleaser.yaml | 8 +- app/backend/agent.go | 69 +++++++ app/backend/backend.go | 5 +- app/backend/context.go | 43 +++-- app/backend/schema.go | 6 +- app/frontend/package.json | 2 +- app/frontend/schema.graphql | 9 + app/frontend/src/components/command-copy.tsx | 27 +++ .../src/components/deploy-an-agent-button.tsx | 149 ++++++++++----- app/frontend/src/components/error-banner.tsx | 2 +- .../components/forms/deploy-agent-form.tsx | 58 +++--- app/frontend/src/components/hosts-table.tsx | 6 +- app/frontend/src/components/icons/os-icon.tsx | 36 ++-- .../src/components/icons/redhat-icon.tsx | 25 +++ .../src/components/icons/ubuntu-icon.tsx | 23 +++ .../src/components/project-metrics-cards.tsx | 18 +- app/frontend/src/lib/utils.ts | 50 ++++- app/frontend/src/queries/agent.ts | 17 ++ app/frontend/src/queries/packages.ts | 11 ++ app/frontend/src/types/index.d.ts | 7 + app/frontend/yarn.lock | 31 +-- cmd/cloudcore-server/config/server.go | 34 +++- .../database/cockroachdb/agent.go | 5 + cmd/cloudcore-server/database/database.go | 5 +- cmd/cloudcore-server/main.go | 5 + cmd/cloudcore-server/providers.go | 12 ++ cmd/cloudcored/commands/version.go | 14 ++ cmd/cloudcored/main.go | 2 + go.mod | 11 +- go.sum | 19 +- internal/agent/config.go | 15 +- pkg/packages/github_release_provider.go | 115 +++++++++++ pkg/packages/github_release_provider_test.go | 26 +++ pkg/packages/packages.go | 26 +++ pkg/version/version.go | 3 + scripts/linux/install.sh | 179 ++++++++---------- 36 files changed, 801 insertions(+), 272 deletions(-) create mode 100644 app/backend/agent.go create mode 100644 app/frontend/src/components/command-copy.tsx create mode 100644 app/frontend/src/components/icons/redhat-icon.tsx create mode 100644 app/frontend/src/components/icons/ubuntu-icon.tsx create mode 100644 app/frontend/src/queries/agent.ts create mode 100644 app/frontend/src/queries/packages.ts create mode 100644 cmd/cloudcore-server/providers.go create mode 100644 cmd/cloudcored/commands/version.go create mode 100644 pkg/packages/github_release_provider.go create mode 100644 pkg/packages/github_release_provider_test.go create mode 100644 pkg/packages/packages.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index efea1fc..c306409 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -7,8 +7,8 @@ builds: env: - CGO_ENABLED=0 targets: - - windows_amd64 - - windows_arm64 +# - windows_amd64 +# - windows_arm64 - darwin_amd64 - darwin_arm64 - linux_amd64 @@ -22,8 +22,8 @@ builds: env: - CGO_ENABLED=0 targets: - - windows_amd64 - - windows_arm64 +# - windows_amd64 +# - windows_arm64 - darwin_amd64 - darwin_arm64 - linux_amd64 diff --git a/app/backend/agent.go b/app/backend/agent.go new file mode 100644 index 0000000..e15d3a8 --- /dev/null +++ b/app/backend/agent.go @@ -0,0 +1,69 @@ +package appbackend + +import ( + "fmt" + "github.com/graphql-go/graphql" +) + +var packageType = graphql.NewObject(graphql.ObjectConfig{ + Name: "Package", + Fields: graphql.Fields{ + "goos": &graphql.Field{Type: graphql.NewNonNull(graphql.String)}, + "goarch": &graphql.Field{Type: graphql.NewNonNull(graphql.String)}, + "version": &graphql.Field{Type: graphql.NewNonNull(graphql.String)}, + "goarm": &graphql.Field{Type: graphql.String}, + }, +}) + +var listPackages = &graphql.Field{ + Type: graphql.NewList(packageType), + Args: graphql.FieldConfigArgument{}, + Resolve: wrapper(func(rctx resolveContext[any]) (any, error) { + return rctx.packages.GetLatestPackages(rctx) + }), +} + +var buildDeployAgentCommand = &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + Args: graphql.FieldConfigArgument{ + "projectId": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)}, + "goos": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)}, + "goarch": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)}, + "generatePsk": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.Boolean)}, + }, + Resolve: wrapper(func(rctx resolveContext[any]) (string, error) { + goos := rctx.getStringArg("goos") + projectId := rctx.getStringArg("projectId") + err := rctx.canAccessProject(projectId) + if err != nil { + return "", err + } + + // Optionally generate a pre-shared key for the agent to use + var psk string + if rctx.getBoolArg("generatePsk") { + psk, err = rctx.db.GeneratePreSharedKey(rctx, projectId) + if err != nil { + return "", fmt.Errorf("generating psk: %w", err) + } + } + + switch goos { + case "linux": + // We can use the same command for all linux architectures + // and the installation script will handle the nuances and + // platform detection processes that need to happen. + return buildLinuxInstallCommand(psk), nil + default: + return "", fmt.Errorf("unsupported goos: %s", goos) + } + }), +} + +func buildLinuxInstallCommand(psk string) string { + if len(psk) == 0 { + return fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/clarkmcc/cloudcore/main/scripts/linux/install.sh | sh") + } else { + return fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/clarkmcc/cloudcore/main/scripts/linux/install.sh | sh -s -- --psk %s", psk) + } +} diff --git a/app/backend/backend.go b/app/backend/backend.go index b07f1e9..fd4da64 100644 --- a/app/backend/backend.go +++ b/app/backend/backend.go @@ -6,6 +6,7 @@ import ( "github.com/clarkmcc/cloudcore/app/backend/middleware" "github.com/clarkmcc/cloudcore/cmd/cloudcore-server/config" "github.com/clarkmcc/cloudcore/cmd/cloudcore-server/database" + "github.com/clarkmcc/cloudcore/pkg/packages" "github.com/gin-gonic/gin" "github.com/graphql-go/graphql" "go.uber.org/fx" @@ -19,9 +20,10 @@ type Server struct { schema graphql.Schema logger *zap.Logger database database.Database + packages packages.Provider } -func New(lc fx.Lifecycle, config *config.Config, database database.Database, logger *zap.Logger) (*Server, error) { +func New(lc fx.Lifecycle, config *config.Config, database database.Database, logger *zap.Logger, packages packages.Provider) (*Server, error) { logger = logger.Named("app-backend") s, err := graphql.NewSchema(schemaConfig) if err != nil { @@ -31,6 +33,7 @@ func New(lc fx.Lifecycle, config *config.Config, database database.Database, log schema: s, logger: logger, database: database, + packages: packages, } r := gin.Default() diff --git a/app/backend/context.go b/app/backend/context.go index 329f6cf..484cb0c 100644 --- a/app/backend/context.go +++ b/app/backend/context.go @@ -5,40 +5,40 @@ import ( "fmt" "github.com/clarkmcc/cloudcore/app/backend/middleware" "github.com/clarkmcc/cloudcore/cmd/cloudcore-server/database" + "github.com/clarkmcc/cloudcore/pkg/packages" "github.com/graphql-go/graphql" "github.com/spf13/cast" "go.uber.org/zap" ) -const contextKeyLogger = "logger" -const contextKeyDatabase = "database" +type contextKeyLogger struct{} +type contextKeyDatabase struct{} +type contextKeyPackages struct{} func (s *Server) graphqlContext(ctx context.Context) context.Context { - ctx = context.WithValue(ctx, contextKeyLogger, s.logger) - ctx = context.WithValue(ctx, contextKeyDatabase, s.database) + ctx = context.WithValue(ctx, contextKeyLogger{}, s.logger) + ctx = context.WithValue(ctx, contextKeyDatabase{}, s.database) + ctx = context.WithValue(ctx, contextKeyPackages{}, s.packages) return ctx } -func logger(ctx context.Context) *zap.Logger { - return ctx.Value(contextKeyLogger).(*zap.Logger) -} - -func db(ctx context.Context) database.Database { - return ctx.Value(contextKeyDatabase).(database.Database) -} - type resolveContext[S any] struct { context.Context - db database.Database - params graphql.ResolveParams - logger *zap.Logger - source S + db database.Database + params graphql.ResolveParams + logger *zap.Logger + packages packages.Provider + source S } func (r *resolveContext[S]) getStringArg(name string) string { return cast.ToString(r.params.Args[name]) } +func (r *resolveContext[S]) getBoolArg(name string) bool { + return cast.ToBool(r.params.Args[name]) +} + func (r *resolveContext[S]) canAccessProject(projectId string) error { sub := middleware.SubjectFromContext(r) if sub == "" { @@ -65,11 +65,12 @@ func wrapper[S any, T any](fn resolverFunc[T, S]) func(params graphql.ResolvePar return nil, fmt.Errorf("invalid source type: %T, expected %T", params.Source, s) } return fn(resolveContext[S]{ - Context: ctx, - db: db(ctx), - logger: logger(ctx), - source: source, - params: params, + Context: ctx, + db: ctx.Value(contextKeyDatabase{}).(database.Database), + packages: ctx.Value(contextKeyPackages{}).(packages.Provider), + logger: ctx.Value(contextKeyLogger{}).(*zap.Logger), + source: source, + params: params, }) } } diff --git a/app/backend/schema.go b/app/backend/schema.go index 2bfcbbf..5a6799b 100644 --- a/app/backend/schema.go +++ b/app/backend/schema.go @@ -14,13 +14,15 @@ var schemaConfig = graphql.SchemaConfig{ "hosts": hostList, "host": hostDetails, "projectMetrics": projectMetrics, + "packages": listPackages, }, }), Mutation: graphql.NewObject(graphql.ObjectConfig{ Name: "Mutation", Fields: graphql.Fields{ - "ensureUser": ensureUser, - "projectCreate": projectCreate, + "ensureUser": ensureUser, + "projectCreate": projectCreate, + "buildDeployAgentCommand": buildDeployAgentCommand, }, }), } diff --git a/app/frontend/package.json b/app/frontend/package.json index 96e869e..f0155f7 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -41,7 +41,7 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", "react-router-dom": "^6.19.0", - "react-usage-bar": "^1.1.22", + "react-usage-bar": "^1.2.0", "sort-by": "^1.2.0", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", diff --git a/app/frontend/schema.graphql b/app/frontend/schema.graphql index c1ad223..a34f1ed 100644 --- a/app/frontend/schema.graphql +++ b/app/frontend/schema.graphql @@ -47,6 +47,7 @@ type Host { } type Mutation { + buildDeployAgentCommand(generatePsk: Boolean!, goarch: String!, goos: String!, projectId: String!): String! ensureUser: [Project] projectCreate(description: String, name: String!): ProjectCreate } @@ -56,6 +57,13 @@ type OsNameCount { osName: String! } +type Package { + goarch: String! + goarm: String + goos: String! + version: String! +} + type Project { created_at: DateTime id: String @@ -80,6 +88,7 @@ type ProjectMetrics { type Query { host(hostId: String!, projectId: String!): Host hosts(projectId: String!): [Host] + packages: [Package] projectMetrics(projectId: String!): ProjectMetrics } diff --git a/app/frontend/src/components/command-copy.tsx b/app/frontend/src/components/command-copy.tsx new file mode 100644 index 0000000..1a6cfdc --- /dev/null +++ b/app/frontend/src/components/command-copy.tsx @@ -0,0 +1,27 @@ +import { useState } from "react"; +import { ClipboardCopy } from "lucide-react"; + +export function CommandCopy(props: { command: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(props.command); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+ {props.command} +
+ +
+ ); +} diff --git a/app/frontend/src/components/deploy-an-agent-button.tsx b/app/frontend/src/components/deploy-an-agent-button.tsx index e72a1c0..583c336 100644 --- a/app/frontend/src/components/deploy-an-agent-button.tsx +++ b/app/frontend/src/components/deploy-an-agent-button.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useState } from "react"; +import { forwardRef, useMemo, useState } from "react"; import { Plus } from "lucide-react"; import { Button, ButtonProps } from "@/components/ui/button.tsx"; import { @@ -8,13 +8,45 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog.tsx"; -import { DeployAgentForm } from "@/components/forms/deploy-agent-form.tsx"; +import { + DeployAgentForm, + DeployAgentFormProps, + DeployAgentFormSchema, +} from "@/components/forms/deploy-agent-form.tsx"; +import { QUERY_LIST_LATEST_PACKAGES } from "@/queries/packages.ts"; +import { useMutation, useQuery } from "@apollo/client"; +import { AgentPlatformDownload, GOARCH, GOARM, GOOS, Package } from "@/types"; +import { goarchToString, goosToString } from "@/lib/utils.ts"; +import { ErrorBanner } from "@/components/error-banner.tsx"; +import { Skeleton } from "@/components/ui/skeleton.tsx"; +import { MUTATION_BUILD_DEPLOY_AGENT_COMMAND } from "@/queries/agent.ts"; +import { z } from "zod"; +import { useProjectId } from "@/hooks/navigation.ts"; +import { CommandCopy } from "@/components/command-copy.tsx"; // type DeployAnAgentButtonProps = ButtonProps & {}; export const DeployAnAgentButton = forwardRef( (props, ref) => { const [open, setOpen] = useState(false); + const [projectId] = useProjectId(); + + const [mutate, { data, loading, error, reset }] = useMutation( + MUTATION_BUILD_DEPLOY_AGENT_COMMAND, + ); + const command = data?.buildDeployAgentCommand ?? null; + + const handleSubmit = async ( + values: z.infer, + ) => { + await mutate({ variables: { ...values, projectId } }); + }; + + function handleDone() { + setOpen(false); + reset(); + } + return ( <> + +
+ +
+ + + )} ); }, ); + +function DeployAgentFormLoader(props: Omit) { + const { data, loading, error } = useQuery<{ packages: Array }>( + QUERY_LIST_LATEST_PACKAGES, + ); + const downloads = useMemo((): AgentPlatformDownload[] => { + if (!data) return []; + + // data.packages returns an array of {goos: string, goarch: string} + // with multiple entries for each goos. We want to group them by goos + // and then map them to the format that the DeployAgentForm expects. + const out: { [key: string]: { goarch: GOARCH; goarm: GOARM }[] } = {}; + data.packages.forEach((p) => { + if (!out[p.goos]) out[p.goos] = []; + if (out[p.goos].find((a) => a.goarch === p.goarch && a.goarm == p.goarm)) + return; + out[p.goos].push({ goarch: p.goarch, goarm: p.goarm }); + }); + return Object.entries(out).map(([goos, goarch]) => ({ + goos: { + display: goosToString(goos), + value: goos as GOOS, + }, + goarch: goarch.map(({ goarch, goarm }) => ({ + display: goarchToString(goarch, goarm), + value: goarch, + })), + })); + }, [data]); + + if (error) { + return ; + } + + if (loading) { + return ; + } + + return ; +} diff --git a/app/frontend/src/components/error-banner.tsx b/app/frontend/src/components/error-banner.tsx index 2769e7d..c9b7132 100644 --- a/app/frontend/src/components/error-banner.tsx +++ b/app/frontend/src/components/error-banner.tsx @@ -6,7 +6,7 @@ type HostsTableProps = { export function ErrorBanner(props: HostsTableProps) { return ( -
+
Oops, that's not right
diff --git a/app/frontend/src/components/forms/deploy-agent-form.tsx b/app/frontend/src/components/forms/deploy-agent-form.tsx index 937c192..ff0f282 100644 --- a/app/frontend/src/components/forms/deploy-agent-form.tsx +++ b/app/frontend/src/components/forms/deploy-agent-form.tsx @@ -14,26 +14,31 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group.tsx"; import { Label } from "@radix-ui/react-dropdown-menu"; import { Button } from "@/components/ui/button.tsx"; import { cn, goosToIcon } from "@/lib/utils.ts"; -import React, { useCallback, useEffect } from "react"; +import React, { useCallback, useEffect, useMemo } from "react"; import { AgentPlatformDownload, DisplayableValue, GOARCH } from "@/types"; import { Switch } from "@/components/ui/switch.tsx"; -const FormSchema = z.object({ - os: z.enum(["linux", "windows", "darwin"], { +export const DeployAgentFormSchema = z.object({ + goos: z.enum(["linux", "windows", "darwin"], { required_error: "Please select an operating system", }), - arch: z.enum(["amd64", "arm64", "386"]), - generate_psk: z.boolean(), + goarch: z.enum(["amd64", "arm64", "386", "arm"]), + generatePsk: z.boolean().default(false), }); -type DeployAgentFormProps = { +export type DeployAgentFormProps = { downloads: AgentPlatformDownload[]; - onSubmit: (values: z.infer) => void; + onSubmit: (values: z.infer) => void; + loading?: boolean; }; -export function DeployAgentForm({ onSubmit, downloads }: DeployAgentFormProps) { - const form = useForm>({ - resolver: zodResolver(FormSchema), +export function DeployAgentForm({ + onSubmit, + downloads, + loading, +}: DeployAgentFormProps) { + const form = useForm>({ + resolver: zodResolver(DeployAgentFormSchema), }); const getArchs = useCallback( @@ -45,8 +50,8 @@ export function DeployAgentForm({ onSubmit, downloads }: DeployAgentFormProps) { [downloads], ); - const os = form.watch("os"); - const arch = form.watch("arch"); + const os = form.watch("goos"); + const arch = form.watch("goarch"); // Watch the os and arch and make sure that when the OS changes, the arch that // is selected is compatible with the OS. If it is not, then select the first @@ -56,23 +61,26 @@ export function DeployAgentForm({ onSubmit, downloads }: DeployAgentFormProps) { const selectedIncompatibleArch = arches.find((a) => a.value === arch) === undefined; if (arches.length > 0 && selectedIncompatibleArch) { - form.setValue("arch", arches[0].value); + form.setValue("goarch", arches[0].value); } }, [os, arch]); + const availableOses = useMemo(() => downloads.length, [downloads]); + const availableArches = useMemo(() => getArchs(os).length, [os]); + return (
( {downloads.map((download) => ( ( {getArchs(os).map((download) => ( (
@@ -141,7 +149,9 @@ export function DeployAgentForm({ onSubmit, downloads }: DeployAgentFormProps) { />
- +
@@ -152,14 +162,14 @@ type OsOptionProps = { os: string; icon: React.ElementType; label: string; - field: ControllerRenderProps, "os">; + field: ControllerRenderProps, "goos">; className?: string; }; function OsOption(props: OsOptionProps) { const Icon = props.icon; return ( - + @@ -184,13 +194,13 @@ type ArchOptionProps = { arch: string; // icon: React.ElementType; label: string; - field: ControllerRenderProps, "arch">; + field: ControllerRenderProps, "goarch">; className?: string; }; function ArchOption(props: ArchOptionProps) { return ( - + diff --git a/app/frontend/src/components/hosts-table.tsx b/app/frontend/src/components/hosts-table.tsx index eaf2652..f606d69 100644 --- a/app/frontend/src/components/hosts-table.tsx +++ b/app/frontend/src/components/hosts-table.tsx @@ -28,7 +28,7 @@ import { useProjectId } from "@/hooks/navigation.ts"; import { useMemo } from "react"; import { Badge } from "@/components/ui/badge.tsx"; import moment from "moment"; -import { getArchitecture, getOsName } from "@/lib/utils.ts"; +import { goarchToString, goosToString, osToString } from "@/lib/utils.ts"; import { OsIcon } from "@/components/icons/os-icon.tsx"; type HostWithGroups = Host & { @@ -90,7 +90,7 @@ export function HostsTable({ hosts }: HostsTableProps) { className="h-5 w-5 mr-2" /> - {getOsName(row.original.osName)} + {osToString(row.original.osName)} )} @@ -106,7 +106,7 @@ export function HostsTable({ hosts }: HostsTableProps) { accessorKey: "kernelArchitecture", header: "Architecture", cell: ({ row }) => { - return getArchitecture(row.original.kernelArchitecture); + return goarchToString(row.original.kernelArchitecture); }, }, { diff --git a/app/frontend/src/components/icons/os-icon.tsx b/app/frontend/src/components/icons/os-icon.tsx index 326b4a2..b8a3b87 100644 --- a/app/frontend/src/components/icons/os-icon.tsx +++ b/app/frontend/src/components/icons/os-icon.tsx @@ -1,20 +1,30 @@ import React, { forwardRef } from "react"; import { AppleIcon } from "@/components/icons/apple-icon.tsx"; import { DebianIcon } from "@/components/icons/debian-icon.tsx"; +import { UbuntuIcon } from "@/components/icons/ubuntu-icon.tsx"; +import { RedHatIcon } from "@/components/icons/redhat-icon.tsx"; type OsIconProps = React.SVGProps & { osName: string; -} +}; -export const OsIcon = forwardRef(({osName, ...rest}, ref) => { - switch (osName) { - case "darwin": - // @ts-expect-error not sure what the issue is here - return - case "debian": - // @ts-expect-error not sure what the issue is here - return - default: - return <> - } -}); +export const OsIcon = forwardRef( + ({ osName, ...rest }, ref) => { + switch (osName) { + case "darwin": + // @ts-expect-error not sure what the issue is here + return ; + case "debian": + // @ts-expect-error not sure what the issue is here + return ; + case "ubuntu": + // @ts-expect-error not sure what the issue is here + return ; + case "redhat": + // @ts-expect-error not sure what the issue is here + return ; + default: + return <>; + } + }, +); diff --git a/app/frontend/src/components/icons/redhat-icon.tsx b/app/frontend/src/components/icons/redhat-icon.tsx new file mode 100644 index 0000000..f3514e7 --- /dev/null +++ b/app/frontend/src/components/icons/redhat-icon.tsx @@ -0,0 +1,25 @@ +import React, { forwardRef } from "react"; +import { cn } from "@/lib/utils.ts"; + +export const RedHatIcon = forwardRef< + SVGSVGElement, + React.SVGProps +>((props, ref) => ( + + + + +)); diff --git a/app/frontend/src/components/icons/ubuntu-icon.tsx b/app/frontend/src/components/icons/ubuntu-icon.tsx new file mode 100644 index 0000000..ed862d2 --- /dev/null +++ b/app/frontend/src/components/icons/ubuntu-icon.tsx @@ -0,0 +1,23 @@ +import React, { forwardRef } from "react"; +import { cn } from "@/lib/utils.ts"; + +export const UbuntuIcon = forwardRef< + SVGSVGElement, + React.SVGProps +>((props, ref) => ( + + + + +)); diff --git a/app/frontend/src/components/project-metrics-cards.tsx b/app/frontend/src/components/project-metrics-cards.tsx index 7c92af8..322fc81 100644 --- a/app/frontend/src/components/project-metrics-cards.tsx +++ b/app/frontend/src/components/project-metrics-cards.tsx @@ -9,7 +9,7 @@ import { import { Skeleton } from "@/components/ui/skeleton.tsx"; import UsageBar from "react-usage-bar"; import { useMemo } from "react"; -import { getOsName } from "@/lib/utils.ts"; +import { osToColorClasses, osToString } from "@/lib/utils.ts"; import { useTheme } from "@/components/theme-provider.tsx"; type ProjectMetricsCardsProps = { @@ -25,7 +25,9 @@ export function ProjectMetricsCards({ const hostsByOsNames = useMemo( () => (metrics?.hostsByOsName ?? []).map((v) => ({ - name: getOsName(v.osName), + name: osToString(v.osName), + className: osToColorClasses(v.osName), + dotClassName: osToColorClasses(v.osName), value: v.count, })), [metrics], @@ -68,17 +70,21 @@ export function ProjectMetricsCards({ { name: "Online", value: metrics?.onlineHosts ?? 0, - color: "#10B981", + className: "bg-green-500 dark:bg-green-500", + dotClassName: "bg-green-500 dark:bg-green-500", }, { name: "Offline", value: metrics?.offlineHosts ?? 0, - color: "#EF4444", + className: "bg-red-500 dark:bg-red-500", + dotClassName: "bg-red-500 dark:bg-red-500", }, ]} - total={2} + total={metrics?.totalHosts ?? 0} showPercentage compactLayout + usageBarContainerClassName="p-0" + usageBarClassName="m-w-full" darkMode={theme === "dark"} /> )} @@ -96,7 +102,7 @@ export function ProjectMetricsCards({ ) : ( = { export type GOOS = "windows" | "linux" | "darwin"; export type GOARCH = "amd64" | "arm64"; +export type GOARM = "5" | "6" | "7"; + +export type Package = { + goos: GOOS; + goarch: GOARCH; + goarm: GOARM; +}; diff --git a/app/frontend/yarn.lock b/app/frontend/yarn.lock index f09dacb..5c4d104 100644 --- a/app/frontend/yarn.lock +++ b/app/frontend/yarn.lock @@ -2266,6 +2266,11 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimist@1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + moment@^2.29.4: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" @@ -2592,10 +2597,14 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react-usage-bar@^1.1.22: - version "1.1.22" - resolved "https://registry.yarnpkg.com/react-usage-bar/-/react-usage-bar-1.1.22.tgz#e33eb51081fd92afd2411ed134fff84b55ffa059" - integrity sha512-hiKeel0OPt+FzbJrfNiYy5+77FTMETqp4keN2R3jI+ASueFlmahHRDi5JO6zMIiM1wWw8bsTfNOG9G5qknAIIw== +react-usage-bar@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/react-usage-bar/-/react-usage-bar-1.2.0.tgz#2f913c17000fb95e0d5bed11c36b85d6cc919200" + integrity sha512-AhSfbtM1QqNrZhlgt6vXt1POfZVGdiq55Hz05/mx+KQfoVozBQRR8yqcPJKLj4ifaPPbaAjaLYcCAiTTb0k2Og== + dependencies: + minimist "1.2.8" + semver "7.5.4" + tslib "^2.6.2" react@^18.2.0: version "18.2.0" @@ -2680,18 +2689,18 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.5.4: +semver@7.5.4, semver@^7.5.4: version "7.5.4" resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -2878,7 +2887,7 @@ ts-invariant@^0.10.3: dependencies: tslib "^2.1.0" -tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0: +tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== diff --git a/cmd/cloudcore-server/config/server.go b/cmd/cloudcore-server/config/server.go index 7b0daea..418edd6 100644 --- a/cmd/cloudcore-server/config/server.go +++ b/cmd/cloudcore-server/config/server.go @@ -1,6 +1,9 @@ package config -import "github.com/spf13/viper" +import ( + "github.com/clarkmcc/cloudcore/pkg/packages" + "github.com/spf13/viper" +) func init() { viper.MustBindEnv("agentServer.port", "AGENT_SERVER_PORT") @@ -9,6 +12,11 @@ func init() { viper.MustBindEnv("auth0.audience", "AUTH0_AUDIENCE") viper.SetDefault("agentServer.port", 10000) viper.SetDefault("appServer.port", 10001) + + viper.SetDefault("packageManagement.provider", GithubReleaseProvider) + viper.SetDefault("packageManagement.githubRelease.owner", "clarkmcc") + viper.SetDefault("packageManagement.githubRelease.repo", "cloudcore") + _ = viper.BindEnv("logging.level", "LOGGING_LEVEL") _ = viper.BindEnv("logging.debug", "LOGGING_DEBUG") viper.MustBindEnv("auth.signingSecret", "AUTH_TOKEN_SIGNING_SECRET") @@ -18,12 +26,13 @@ func init() { } type Config struct { - AgentServer AgentServer `json:"agentServer"` - AppServer AppServer `json:"appServer"` - Logging Logging `json:"logging"` - Auth serverAuth `json:"auth"` - Auth0 Auth0 `json:"auth0"` - Database serverDatabase `json:"database"` + AgentServer AgentServer `json:"agentServer"` + AppServer AppServer `json:"appServer"` + Logging Logging `json:"logging"` + Auth serverAuth `json:"auth"` + Auth0 Auth0 `json:"auth0"` + Database serverDatabase `json:"database"` + PackageManagement PackageManagementConfig `json:"packageManagement"` } type serverAuth struct { @@ -61,6 +70,17 @@ type Auth0 struct { Audience string `json:"audience"` } +type PackageManagementProvider string + +const ( + GithubReleaseProvider PackageManagementProvider = "github-release" +) + +type PackageManagementConfig struct { + Provider PackageManagementProvider `json:"provider"` + GithubRelease packages.GithubReleaseProviderConfig `json:"githubRelease,omitempty"` +} + func New() (*Config, error) { var cfg Config err := viper.Unmarshal(&cfg) diff --git a/cmd/cloudcore-server/database/cockroachdb/agent.go b/cmd/cloudcore-server/database/cockroachdb/agent.go index e792b86..9cced7e 100644 --- a/cmd/cloudcore-server/database/cockroachdb/agent.go +++ b/cmd/cloudcore-server/database/cockroachdb/agent.go @@ -208,6 +208,11 @@ func (d *Database) addAgentEvent(tx *sqlx.Tx, agentID string, event types.AgentE return nil } +func (d *Database) GeneratePreSharedKey(ctx context.Context, projectId string) (key string, err error) { + return key, d.db.GetContext(ctx, &key, `INSERT INTO agent_psk (project_id, name, description) VALUES ($1, $2, $3) RETURNING key`, + projectId, "Deploy Agent", "Generated during 'deploy new agent' process") +} + func handleRollback(err *error, tx *sqlx.Tx) { if *err != nil { multierr.AppendFunc(err, tx.Rollback) diff --git a/cmd/cloudcore-server/database/database.go b/cmd/cloudcore-server/database/database.go index fea8e62..0400561 100644 --- a/cmd/cloudcore-server/database/database.go +++ b/cmd/cloudcore-server/database/database.go @@ -58,8 +58,11 @@ type AppDatabase interface { // GetEventLogsByHost returns the events logs for the host with the given host ID. GetEventLogsByHost(ctx context.Context, hostId string, limit int) (out []types.AgentEventLog, err error) - // GetDashboardMetrics returns the dashboard metrics for the given project ID. + // GetProjectMetrics returns the dashboard metrics for the given project ID. GetProjectMetrics(ctx context.Context, projectId string) (*types.ProjectMetrics, error) + + // GeneratePreSharedKey generates a new pre-shared key for the given project ID. + GeneratePreSharedKey(ctx context.Context, projectId string) (string, error) } func New(cfg *config.Config, logger *zap.Logger) (Database, error) { diff --git a/cmd/cloudcore-server/main.go b/cmd/cloudcore-server/main.go index 1bc134f..6945036 100644 --- a/cmd/cloudcore-server/main.go +++ b/cmd/cloudcore-server/main.go @@ -10,6 +10,7 @@ import ( "github.com/clarkmcc/cloudcore/internal/logger" "github.com/clarkmcc/cloudcore/internal/rpc" "github.com/clarkmcc/cloudcore/internal/token" + "github.com/clarkmcc/cloudcore/pkg/packages" "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/zap" @@ -23,10 +24,14 @@ func main() { return signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) }), fx.Provide(config.New), + fx.Provide(componentConfigs), fx.Provide(token.NewSigner), fx.Provide(database.New), fx.Provide(appbackend.New), fx.Provide(server.New), + fx.Provide(fx.Annotate( + packages.NewGithubReleaseProvider, + fx.As(new(packages.Provider)))), fx.Provide(func(config *config.Config) *zap.Logger { return logger.New(config.Logging.Level, config.Logging.Debug) }), diff --git a/cmd/cloudcore-server/providers.go b/cmd/cloudcore-server/providers.go new file mode 100644 index 0000000..5617673 --- /dev/null +++ b/cmd/cloudcore-server/providers.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/clarkmcc/cloudcore/cmd/cloudcore-server/config" + "github.com/clarkmcc/cloudcore/pkg/packages" +) + +// componentConfigs is a fx.Provider function that returns component-specific configurations +// that are part of the global configuration. +func componentConfigs(config *config.Config) packages.GithubReleaseProviderConfig { + return config.PackageManagement.GithubRelease +} diff --git a/cmd/cloudcored/commands/version.go b/cmd/cloudcored/commands/version.go new file mode 100644 index 0000000..59e084a --- /dev/null +++ b/cmd/cloudcored/commands/version.go @@ -0,0 +1,14 @@ +package commands + +import ( + "github.com/clarkmcc/cloudcore/pkg/utils" + "github.com/clarkmcc/cloudcore/pkg/version" + "github.com/spf13/cobra" +) + +var Version = &cobra.Command{ + Use: "version", + Run: func(cmd *cobra.Command, args []string) { + utils.PrintStruct(version.Get()) + }, +} diff --git a/cmd/cloudcored/main.go b/cmd/cloudcored/main.go index 89339c9..646d167 100644 --- a/cmd/cloudcored/main.go +++ b/cmd/cloudcored/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/clarkmcc/cloudcore/cmd/cloudcored/commands" "github.com/clarkmcc/cloudcore/internal/agent" "github.com/clarkmcc/cloudcore/internal/logger" "github.com/clarkmcc/cloudcore/internal/sysinfo" @@ -56,6 +57,7 @@ var cmd = &cobra.Command{ func init() { cmd.PersistentFlags().String("psk", "", "Pre-shared key for authenticating with the server") cmd.PersistentFlags().Bool("insecure-skip-verify", false, "Whether to skip verifying the server's TLS certificate") + cmd.AddCommand(commands.Version) } func main() { diff --git a/go.mod b/go.mod index 73cbccb..b79100f 100644 --- a/go.mod +++ b/go.mod @@ -4,18 +4,23 @@ go 1.21.1 require ( github.com/auth0/go-jwt-middleware/v2 v2.1.0 + github.com/clarkmcc/brpc v0.0.0-20231123175550-0f3af44fb169 github.com/gin-gonic/gin v1.9.1 github.com/golang-jwt/jwt/v5 v5.1.0 github.com/golang-migrate/migrate/v4 v4.16.2 github.com/golang/protobuf v1.5.3 + github.com/google/go-github/v56 v56.0.0 github.com/graphql-go/graphql v0.8.1 github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.9 github.com/magiconair/properties v1.8.7 + github.com/quic-go/quic-go v0.40.0 github.com/rs/cors v1.10.1 github.com/shirou/gopsutil/v3 v3.23.10 + github.com/spf13/cast v1.5.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.17.0 + github.com/stretchr/testify v1.8.4 go.uber.org/fx v1.20.1 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.26.0 @@ -28,8 +33,8 @@ require ( github.com/bytedance/sonic v1.10.2 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect - github.com/clarkmcc/brpc v0.0.0-20231123175550-0f3af44fb169 // indirect github.com/cockroachdb/cockroach-go/v2 v2.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -39,6 +44,7 @@ require ( github.com/go-playground/validator/v10 v10.16.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/uuid v1.3.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -57,15 +63,14 @@ require ( github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect - github.com/quic-go/quic-go v0.40.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect - github.com/spf13/cast v1.5.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect diff --git a/go.sum b/go.sum index 3991244..a81dc39 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,6 @@ github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/clarkmcc/brpc v0.0.0-20231108204027-edcb7338e46c h1:SAdJlSyal/8uvBiq9N8Z8SNCkdUn+NHJG2b0V72HPtk= -github.com/clarkmcc/brpc v0.0.0-20231108204027-edcb7338e46c/go.mod h1:FUkwJJi50LAKvzJv/Ibr/uOAwNWdllEu36IqEuGlANI= github.com/clarkmcc/brpc v0.0.0-20231123175550-0f3af44fb169 h1:hLi6WXXhtU42VpZqSvavZLEHjT297mta80MU8igLK80= github.com/clarkmcc/brpc v0.0.0-20231123175550-0f3af44fb169/go.mod h1:FUkwJJi50LAKvzJv/Ibr/uOAwNWdllEu36IqEuGlANI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -109,6 +107,8 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -178,6 +178,10 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= +github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -322,9 +326,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= @@ -439,8 +444,6 @@ go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= @@ -475,8 +478,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -502,8 +503,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -678,8 +677,6 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/agent/config.go b/internal/agent/config.go index 4d931f1..9bdd614 100644 --- a/internal/agent/config.go +++ b/internal/agent/config.go @@ -1,6 +1,7 @@ package agent import ( + "bytes" "errors" "fmt" "github.com/clarkmcc/cloudcore/pkg/utils" @@ -103,16 +104,18 @@ func getPskFromFile(_ string) ([]byte, error) { switch runtime.GOOS { case "linux": filename = "/etc/cloudcored/psk" - case "darwin": + default: dir, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("getting home dir: %w", err) } filename = filepath.Join(dir, ".cloudcored", "psk") - default: - return nil, fmt.Errorf("reading psk from file not supported on %s", runtime.GOOS) } - return os.ReadFile(filename) + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + return bytes.TrimSuffix(b, []byte("\n")), nil } func writePskToFile(psk string) error { @@ -120,14 +123,12 @@ func writePskToFile(psk string) error { switch runtime.GOOS { case "linux": filename = "/etc/cloudcored/psk" - case "darwin": + default: dir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("getting home dir: %w", err) } filename = filepath.Join(dir, ".cloudcored", "psk") - default: - return fmt.Errorf("saving psk to file not supported on %s", runtime.GOOS) } err := os.MkdirAll(filepath.Dir(filename), 0600) if err != nil { diff --git a/pkg/packages/github_release_provider.go b/pkg/packages/github_release_provider.go new file mode 100644 index 0000000..ab65145 --- /dev/null +++ b/pkg/packages/github_release_provider.go @@ -0,0 +1,115 @@ +package packages + +import ( + "context" + "errors" + "github.com/google/go-github/v56/github" + "go.uber.org/zap" + "net/http" + "strings" +) + +const agentBinaryName = "cloudcored" + +var _ Provider = (*GithubReleaseProvider)(nil) + +type GithubReleaseProviderConfig struct { + Owner string + Repo string +} + +type GithubReleaseProvider struct { + *github.Client + logger *zap.Logger + + owner string + repo string +} + +func (g *GithubReleaseProvider) GetLatestPackages(ctx context.Context) ([]Package, error) { + rel, res, err := g.Repositories.GetLatestRelease(ctx, g.owner, g.repo) + if res.StatusCode == http.StatusNotFound { + return nil, ErrNoPackages + } + if err != nil { + return nil, err + } + var packages []Package + for _, ga := range rel.Assets { + if ga.BrowserDownloadURL == nil { + continue + } + a, err := parseAssetName(ga.GetName()) + if err != nil { + g.logger.Warn("parsing asset name", zap.Error(err), zap.String("name", ga.GetName())) + continue + } + if a.Name != agentBinaryName { + continue + } + packages = append(packages, Package{ + GOOS: a.GOOS, + GOARCH: a.GOARCH, + GOARM: a.GOARM, + Version: a.Version, + DownloadURL: *ga.BrowserDownloadURL, + }) + } + return packages, nil +} + +func (g *GithubReleaseProvider) FindLatestPackage(ctx context.Context, goos, goarch, goarm string) (*Package, error) { + packages, err := g.GetLatestPackages(ctx) + if err != nil { + return nil, err + } + for _, p := range packages { + if p.GOOS == goos && p.GOARCH == goarch && p.GOARM == goarm { + return &p, nil + } + } + return nil, ErrNoPackages +} + +func NewGithubReleaseProvider(config GithubReleaseProviderConfig, logger *zap.Logger) *GithubReleaseProvider { + return &GithubReleaseProvider{ + Client: github.NewClient(nil), + owner: config.Owner, + repo: config.Repo, + logger: logger, + } +} + +type asset struct { + Name string + Version string + GOOS string + GOARCH string + GOARM string +} + +func parseAssetName(name string) (*asset, error) { + parts := strings.Split(trimSuffix(name), "_") + if len(parts) < 4 { + return nil, errors.New("invalid asset name") + } + var goarm string + if len(parts) >= 5 { + goarm = parts[4] + } + return &asset{ + Name: parts[0], + Version: parts[1], + GOOS: parts[2], + GOARCH: parts[3], + GOARM: goarm, + }, nil +} + +func trimSuffix(name string) string { + name = strings.TrimSuffix(name, ".deb") + name = strings.TrimSuffix(name, ".apk") + name = strings.TrimSuffix(name, ".rpm") + name = strings.TrimSuffix(name, ".exe") + return name +} diff --git a/pkg/packages/github_release_provider_test.go b/pkg/packages/github_release_provider_test.go new file mode 100644 index 0000000..cc8aa40 --- /dev/null +++ b/pkg/packages/github_release_provider_test.go @@ -0,0 +1,26 @@ +package packages + +import ( + "context" + "github.com/clarkmcc/cloudcore/pkg/utils" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "testing" +) + +func TestGithubReleaseProvider_FindLatestPackage(t *testing.T) { + provider := NewGithubReleaseProvider(GithubReleaseProviderConfig{ + Owner: "clarkmcc", + Repo: "cloudcore", + }, zap.NewNop()) + + packages, err := provider.GetLatestPackages(context.Background()) + assert.NoError(t, err) + assert.Greater(t, len(packages), 0) + utils.PrintStruct(packages) + + pack, err := provider.FindLatestPackage(context.Background(), "linux", "amd64", "") + assert.NoError(t, err) + assert.NotNil(t, pack) + utils.PrintStruct(pack) +} diff --git a/pkg/packages/packages.go b/pkg/packages/packages.go new file mode 100644 index 0000000..053037b --- /dev/null +++ b/pkg/packages/packages.go @@ -0,0 +1,26 @@ +package packages + +import ( + "context" + "errors" +) + +type Package struct { + GOOS string `json:"goos"` + GOARCH string `json:"goarch"` + GOARM string `json:"goarm"` + Version string `json:"version"` + DownloadURL string `json:"downloadUrl"` +} + +var ErrNoPackages = errors.New("no packages found") + +type Provider interface { + // GetLatestPackages returns all the available packages for the latest release and + // returns ErrNoPackages if no packages can be found. + GetLatestPackages(ctx context.Context) ([]Package, error) + + // FindLatestPackage returns the latest package for the given GOOS, GOARCH, and GOARM + // and returns ErrNoPackages if no package can be found. + FindLatestPackage(ctx context.Context, goos, goarch, goarm string) (*Package, error) +} diff --git a/pkg/version/version.go b/pkg/version/version.go index 685cb72..b1267a4 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -16,9 +16,11 @@ import ( var Version = "x.y.z" var Hash = "aabbccdd" +var GOARM = "" type Info struct { GoVersion string `json:"go"` + GOARM string `json:"goarm,omitempty"` Compiler string `json:"compiler"` Platform string `json:"platform"` Version string `json:"version"` @@ -31,6 +33,7 @@ func Get() Info { return Info{ GoVersion: runtime.Version(), Compiler: runtime.Compiler, + GOARM: GOARM, Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), Version: Version, Hash: Hash, diff --git a/scripts/linux/install.sh b/scripts/linux/install.sh index e3785e1..4c5422f 100644 --- a/scripts/linux/install.sh +++ b/scripts/linux/install.sh @@ -1,122 +1,101 @@ #!/bin/bash -CAN_ROOT='' -SUDO='' +set -eu -if [ "$(id -u)" = 0 ]; then - CAN_ROOT=1 - SUDO="" - elif type sudo >/dev/null; then - CAN_ROOT=1 - SUDO="sudo" - elif type doas >/dev/null; then - CAN_ROOT=1 - SUDO="doas" -fi - -if [ "$CAN_ROOT" != "1" ]; then - echo "could not obtain root or sudo access, aborting install. re-run this script as root or setup sudo" - return -fi - -# Function to fetch the latest version number from GitHub -get_latest_version() { - curl -L -s \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/clarkmcc/cloudcore/releases/latest | \ - grep '"tag_name":' | \ - cut -d '"' -f 4 -} - -# Function to determine OS -get_os() { - if [ -f /etc/debian_version ]; then - echo "debian" - elif [ -f /etc/alpine-release ]; then - echo "alpine" - elif [ -f /etc/arch-release ]; then - echo "arch" - elif [ -f /etc/redhat-release ]; then - echo "rhel" - else - echo "unsupported" - fi +# Function definitions +log() { + echo "$@" >&2 } -# Function to determine architecture using lscpu -get_architecture() { - lscpu | grep Architecture | cut -d ':' -f 2 | sed 's/ //g' +error() { + log "ERROR: $@" + exit 1 } -# Function to download and install the package -install_package() { - local version=$1 - local os=$2 - local arch=$3 - local base_url="https://github.com/clarkmcc/cloudcore/releases/download/${version}" - local package_name="" - local installer_command="" - local package_extension="" +detect_os_and_arch() { + OS="" + ARCH=$(uname -m) + PACKAGE_TYPE="" + + if [ -f /etc/os-release ]; then + . /etc/os-release + case "$ID" in + debian|ubuntu|linuxmint) + OS="debian" + PACKAGE_TYPE="deb" + ;; + centos|fedora|rhel|rocky|almalinux) + OS="rhel" + PACKAGE_TYPE="rpm" + ;; + alpine) + OS="alpine" + PACKAGE_TYPE="apk" + ;; + *) + OS="unsupported" + ;; + esac + fi - echo "Installing version $version for $os on $arch architecture" + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + armv7l) ARCH="arm5" ;; + *) ARCH="unsupported" ;; + esac - # Determine the package name, extension, and installer command based on OS and architecture - if [ "$os" = "debian" ]; then - package_extension=".deb" - installer_command="${SUDO} dpkg -i" - case $arch in - x86_64) package_name="cloudcored_${version}_linux_amd64${package_extension}" ;; - aarch64) package_name="cloudcored_${version}_linux_arm64${package_extension}" ;; - armv7l) package_name="cloudcored_${version}_linux_arm5${package_extension}" ;; - esac - elif [ "$os" = "rhel" ] && [ "$arch" = "aarch64" ]; then - package_extension=".rpm" - installer_command="${SUDO} rpm -i" - package_name="cloudcored_${version}_linux_aarch64${package_extension}" - else - echo "Unsupported OS or architecture: $os, $arch" - return + if [ -z "$OS" ] || [ "$ARCH" = "unsupported" ]; then + error "Unsupported OS or architecture: $OS, $ARCH" fi +} - # Construct the download URL - local url="${base_url}/${package_name}" +fetch_latest_version_and_construct_package_url() { + VERSION=$(curl -L -s -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/clarkmcc/cloudcore/releases/latest | \ + grep '"tag_name":' | cut -d '"' -f 4) + PACKAGE_URL="https://github.com/clarkmcc/cloudcore/releases/download/${VERSION}/cloudcored_${VERSION}_linux_${ARCH}.${PACKAGE_TYPE}" +} - # Download and install the package - if [ -n "$package_name" ]; then - echo -n "Fetching $url: " - curl -L -o "$package_name" "$url" - echo "done" - echo "Installing package" - $installer_command "$package_name" - rm "$package_name" +check_root_privileges() { + if [ "$(id -u)" -eq 0 ]; then + SUDO="" + elif type sudo >/dev/null 2>&1; then + SUDO="sudo" + else + error "Root privileges required. Please run this script as root or install sudo." fi } -# Main installation process -version=$(get_latest_version) -os=$(get_os) -arch=$(get_architecture) +install_package() { + log "Installing CloudCore version $VERSION for $OS on $ARCH architecture" + curl -L -o cloudcore_package "${PACKAGE_URL}" -# Parse command-line arguments for the --psk parameter -while [ $# -gt 0 ]; do - case "$1" in - --psk) - CLOUDCORE_PSK="$2" - shift 2 + case "$PACKAGE_TYPE" in + deb) + $SUDO dpkg -i cloudcore_package + ;; + rpm) + $SUDO rpm -i cloudcore_package + ;; + apk) + $SUDO apk add --allow-untrusted cloudcore_package ;; *) - break + error "Installation method not supported for package type: $PACKAGE_TYPE" ;; esac -done - + rm cloudcore_package +} -if [ "$os" = "unsupported" ] || [ "$arch" = "unsupported" ]; then - echo "Error: Unsupported OS or architecture." - exit 1 -fi +main() { + detect_os_and_arch + fetch_latest_version_and_construct_package_url + check_root_privileges + install_package + log "CloudCore installation complete!" +} -export CLOUDCORE_PSK -install_package "$version" "$os" "$arch" -unset CLOUDCORE_PSK +# Execute main function +main