From 82fcd41ef47342d77f189189c4e81db4112db364 Mon Sep 17 00:00:00 2001 From: vanDeventer Date: Tue, 27 May 2025 14:12:40 +0200 Subject: [PATCH 001/191] redo configuration to have configurable services --- components/{device.go => host.go} | 0 components/husk.go | 2 +- components/service.go | 11 +- components/system.go | 54 ++++++++-- components/uasset.go | 5 + usecases/authentication.go | 57 ++++++---- usecases/configuration.go | 144 ++++++++++++++------------ usecases/docs.go | 24 +++-- usecases/registration.go | 2 + usecases/serversNhandlers.go | 8 +- usecases/{packing.go => utilities.go} | 0 11 files changed, 197 insertions(+), 110 deletions(-) rename components/{device.go => host.go} (100%) rename usecases/{packing.go => utilities.go} (100%) diff --git a/components/device.go b/components/host.go similarity index 100% rename from components/device.go rename to components/host.go diff --git a/components/husk.go b/components/husk.go index 431562a..0c56363 100644 --- a/components/husk.go +++ b/components/husk.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Synecdoque + * Copyright (c) 2025 Synecdoque * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/components/service.go b/components/service.go index ff8a14c..503a98b 100644 --- a/components/service.go +++ b/components/service.go @@ -26,7 +26,7 @@ package components type Service struct { ID int `json:"-"` // Id assigned by the Service Registrar Definition string `json:"definition"` // Service definition or purpose - SubPath string `json:"-"` // The URL subpath after the resource's + SubPath string `json:"subpath"` // The URL subpath after the resource's Details map[string][]string `json:"details"` // Metadata or details about the service RegPeriod int `json:"registrationPeriod"` // The period until the registrar is expecting a sign of life RegTimestamp string `json:"-"` // the creation date in the Service Registry to ensure that reRegistration is with the same record @@ -113,10 +113,11 @@ func MergeDetails(map1, map2 map[string][]string) map[string][]string { // A Cervice is a consumed service type Cervice struct { - Definition string - Details map[string][]string - Nodes map[string][]string - Protos []string + IReferentce string // Internal reference when consuming more than one service of the same type + Definition string // Service definition or purpose + Details map[string][]string + Nodes map[string][]string + Protos []string } // Cervises is a collection of "Cervice" structs diff --git a/components/system.go b/components/system.go index abccc4b..a9ac4cb 100644 --- a/components/system.go +++ b/components/system.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Synecdoque + * Copyright (c) 2025 Synecdoque * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,14 +24,17 @@ package components import ( "context" "fmt" + "io" + "net/http" "os" "os/signal" + "strings" "syscall" ) -// System struct aggragates an Arrowhead compliant system +// System struct aggregates an Arrowhead compliant system type System struct { - Name string `json:"systemname"` + Name string `json:"systemName"` Host *HostingDevice // the system runs on a device Husk *Husk // the system aggregates a "husk" (a wrapper or a shell) UAssets map[string]*UnitAsset // the system aggregates "asset", which is made up of one or more unit-asset @@ -43,9 +46,8 @@ type System struct { // CoreSystem struct holds details about the core system included in the configuration file type CoreSystem struct { - Name string `json:"coresystem"` - Url string `json:"url"` - Certificate string `json:"-"` + Name string `json:"coreSystem"` + Url string `json:"url"` } // NewSystem instantiates the new system and gathers the host information @@ -61,6 +63,46 @@ func NewSystem(name string, ctx context.Context) System { return newSystem } +// GetRunningCoreSystemURL returns the URL of a running core system based on the provided type. +// When systemType is "serviceregistrar", it verifies the service is the lead registrar by checking +// its /status endpoint response. For other core system types, it simply tests that the URL is accessible. +func GetRunningCoreSystemURL(sys *System, systemType string) (string, error) { + for _, core := range sys.CoreS { + if core.Name == systemType { + // Special logic for the service registrar: check the status endpoint + if systemType == "serviceregistrar" { + statusURL := core.Url + "/status" + resp, err := http.Get(statusURL) + if err != nil { + fmt.Printf("error checking service registrar status at %s: %v\n", statusURL, err) + continue // Try the next core system instance, if any. + } + bodyBytes, err := io.ReadAll(resp.Body) + resp.Body.Close() // Always close the response body when done. + if err != nil { + fmt.Printf("error reading response from %s: %v\n", statusURL, err) + continue + } + // Verify status response + if strings.HasPrefix(string(bodyBytes), "lead Service Registrar since") { + fmt.Printf("Lead service registrar found at: %s\n", core.Url) + return core.Url, nil + } + } else { + // For other core systems, verify that the service is accessible. + resp, err := http.Get(core.Url) + if err != nil { + fmt.Printf("error checking %s at %s: %v\n", systemType, core.Url, err) + continue + } + resp.Body.Close() + return core.Url, nil + } + } + } + return "", fmt.Errorf("failed to locate running core system of type %s", systemType) +} + // The following code is used only for issues support on GitHub @sdoque -------------------------- var ( AppName string diff --git a/components/uasset.go b/components/uasset.go index 300ba79..65ee1e0 100644 --- a/components/uasset.go +++ b/components/uasset.go @@ -33,3 +33,8 @@ type UnitAsset interface { GetDetails() map[string][]string Serving(w http.ResponseWriter, r *http.Request, servicePath string) } + +// HasTraits is an interface that defines a method to get traits of a UnitAsset. +type HasTraits interface { + GetTraits() any // or interface{} in older Go +} diff --git a/usecases/authentication.go b/usecases/authentication.go index 134ff25..0e531d2 100644 --- a/usecases/authentication.go +++ b/usecases/authentication.go @@ -29,6 +29,7 @@ import ( "encoding/pem" "fmt" "log" + "net" "net/http" "strings" @@ -40,18 +41,28 @@ func RequestCertificate(sys *components.System) { // Generate ECDSA Private Key privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - log.Fatalf("Failed to generate private key: %v", err) + log.Fatalf("Failed to generate private key: %v\n", err) } sys.Husk.Pkey = privateKey + dnsNames := []string{"localhost"} + var ipAddrs []net.IP + for _, ipStr := range sys.Host.IPAddresses { + ip := net.ParseIP(ipStr) + if ip != nil { + ipAddrs = append(ipAddrs, ip) + } + } csrTemplate := x509.CertificateRequest{ Subject: sys.Husk.DName, + DNSNames: dnsNames, // this is the SAN DNS + IPAddresses: ipAddrs, // this is the SAN IPs SignatureAlgorithm: x509.ECDSAWithSHA256, } csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privateKey) if err != nil { - log.Fatalf("Failed to create CSR: %v", err) + log.Fatalf("Failed to create CSR: %v\n", err) return } @@ -61,7 +72,7 @@ func RequestCertificate(sys *components.System) { // Send the CSR to the CA and receive the certificate in response response, err := sendCSR(sys, csrPEM) if err != nil { - log.Printf("certification failure: %v", err) + log.Printf("certification failure: %v\n", err) return } @@ -71,7 +82,7 @@ func RequestCertificate(sys *components.System) { // Get CA's certificate caCert, err := getCACertificate(sys) if err != nil { - log.Printf("failed to obtain CA's certificate: %v", err) + log.Printf("failed to obtain CA's certificate: %v\n", err) return } sys.Husk.CA_cert = caCert @@ -79,13 +90,13 @@ func RequestCertificate(sys *components.System) { // Load CA certificate caCertPool := x509.NewCertPool() if ok := caCertPool.AppendCertsFromPEM([]byte(caCert)); !ok { - log.Fatalf("Failed to append CA certificate to pool") + log.Fatalf("Failed to append CA certificate to pool\n") } // Prepare the client's certificate and key for TLS configuration clientCert, err := prepareClientCertificate(sys.Husk.Certificate, sys.Husk.Pkey) if err != nil { - log.Fatalf("Failed to prepare client certificate: %v", err) + log.Fatalf("Failed to prepare client certificate: %v\n", err) } // Configure Transport Layer Security (TLS) @@ -100,7 +111,7 @@ func RequestCertificate(sys *components.System) { fmt.Printf("System %s's parsed Certificate:\n", sys.Name) cert, err := x509.ParseCertificate(clientCert.Certificate[0]) if err != nil { - log.Printf("failed to parse certificate: %v", err) + log.Printf("failed to parse certificate: %v\n", err) return } fmt.Printf(" Subject: %s\n", cert.Subject) @@ -108,6 +119,9 @@ func RequestCertificate(sys *components.System) { fmt.Printf(" Serial Number: %d\n", cert.SerialNumber) fmt.Printf(" Not Before: %s\n", cert.NotBefore) fmt.Printf(" Not After: %s\n", cert.NotAfter) + fmt.Printf(" DNS Names: %v\n", cert.DNSNames) + fmt.Printf(" IP Addresses: %v\n", cert.IPAddresses) + } func sendCSR(sys *components.System, csrPEM []byte) (string, error) { @@ -144,18 +158,25 @@ func sendCSR(sys *components.System, csrPEM []byte) (string, error) { // getCACertificate gets the CA's certificate necessary for the dual server-client authentication in the TLS setup func getCACertificate(sys *components.System) (string, error) { - var err error - coreUAurl := "" - for _, cSys := range sys.CoreS { - core := cSys - if core.Name == "ca" { - coreUAurl = core.Url - } - } - if coreUAurl == "" { - return "", fmt.Errorf("failed to locate certificate authority: %w", err) + // var err error + // coreUAurl := "" + // for _, cSys := range sys.CoreS { + // core := cSys + // if core.Name == "ca" { + // coreUAurl = core.Url + // } + // } + // if coreUAurl == "" { + // return "", fmt.Errorf("failed to locate certificate authority: %w", err) + // } + + // Get the URL of the CA's configuration + coreUAurl, err := components.GetRunningCoreSystemURL(sys, "ca") // Assuming the first core system is the CA + if err != nil { + return "", fmt.Errorf("failed to get CA URL: %w", err) } - url := strings.TrimSuffix(coreUAurl, "ification") // the configuration file address to the CA includes the unit asset + // Remove the "ification" suffix from the URL to get the CA's address + url := strings.TrimSuffix(coreUAurl, "ification") // Make a GET request to the CA's endpoint resp, err := http.Get(url) diff --git a/usecases/configuration.go b/usecases/configuration.go index 4862200..7a7a5e8 100644 --- a/usecases/configuration.go +++ b/usecases/configuration.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Synecdoque + * Copyright (c) 2025 Synecdoque * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,6 @@ package usecases import ( - "crypto/x509/pkix" "encoding/json" "fmt" "os" @@ -30,14 +29,21 @@ import ( "github.com/sdoque/mbaigo/components" ) +// configurableAsset is a struct that contains the name of the asset and its +// configurable details and services +type ConfigurableAsset struct { + Name string `json:"name"` + Details map[string][]string `json:"details"` + Services []components.Service `json:"services"` + Traits []json.RawMessage `json:"traits"` +} + // templateOut is the stuct used to prepare the systemconfig.json file type templateOut struct { - CName string `json:"systemname"` - UAsset []components.UnitAsset `json:"unit_assets"` - CServices []components.Service `json:"services"` - Protocols map[string]int `json:"protocolsNports"` - PKIdetails pkix.Name `json:"distinguishedName"` - CCoreS []components.CoreSystem `json:"coreSystems"` + CName string `json:"systemname"` + Assets []ConfigurableAsset `json:"unit_assets"` + Protocols map[string]int `json:"protocolsNports"` + CCoreS []components.CoreSystem `json:"coreSystems"` } // configFileIn is used to extact out the information of the systemconfig.json file @@ -46,77 +52,95 @@ type templateOut struct { type configFileIn struct { CName string `json:"systemname"` rawResources []json.RawMessage `json:"-"` - CServices []components.Service `json:"services"` Protocols map[string]int `json:"protocolsNports"` - PKIdetails pkix.Name `json:"distinguishedName"` CCoreS []components.CoreSystem `json:"coreSystems"` } -// Configure read the system configuration JSON file to get the deployment details. +// Configure reads the system configuration JSON file to get the deployment details. // If the file is missing, it generates a default systemconfig.json file and shuts down the system -func Configure(sys *components.System) ([]json.RawMessage, []components.Service, error) { - - var rawBytes []json.RawMessage // the mbaigo library does not know about the unit asset's structure (defined in the file thing.go and not part of the library) - var servicesList []components.Service // this is the list of services for each unit asset +func Configure(sys *components.System) ([]json.RawMessage, error) { // prepare content of configuration file var defaultConfig templateOut + // var servicesList []components.Service // this is the list of services for each unit asset + + var assetTemplate components.UnitAsset + for _, ua := range sys.UAssets { + assetTemplate = *ua // this creates a copy (value, not reference) + break // stop after the first entry + } + servicesTemplate := getServicesList(assetTemplate) + + confAsset := ConfigurableAsset{ + Name: assetTemplate.GetName(), + Details: assetTemplate.GetDetails(), + Services: servicesTemplate, + } + + // If the asset exposes traits, serialize them and store as raw JSON + if assetWithTraits, ok := assetTemplate.(components.HasTraits); ok { + if traits := assetWithTraits.GetTraits(); traits != nil { + traitJSON, err := json.Marshal(traits) + if err == nil { + confAsset.Traits = []json.RawMessage{traitJSON} + } else { + fmt.Println("Warning: could not marshal traits:", err) + } + } + } + defaultConfig.CName = sys.Name defaultConfig.Protocols = sys.Husk.ProtoPort - defaultConfig.UAsset = getFirstAsset(sys.UAssets) - originalSs := getServicesList(defaultConfig.UAsset[0]) - defaultConfig.CServices = originalSs - - defaultConfig.PKIdetails.CommonName = "arrowhead.eu" - defaultConfig.PKIdetails.Country = []string{"SE"} - defaultConfig.PKIdetails.Province = []string{"Norrbotten"} - defaultConfig.PKIdetails.Locality = []string{"Luleaa"} - defaultConfig.PKIdetails.Organization = []string{"Luleaa University of Technology"} - defaultConfig.PKIdetails.OrganizationalUnit = []string{"CPS"} + defaultConfig.Assets = []ConfigurableAsset{confAsset} // this is a list of unit assets serReg := components.CoreSystem{ - Name: "serviceregistrar", - Url: "http://localhost:20102/serviceregistrar/registry", - Certificate: ".X509pubKey", + Name: "serviceregistrar", + Url: "http://localhost:20102/serviceregistrar/registry", } orches := components.CoreSystem{ - Name: "orchestrator", - Url: "http://localhost:20103/orchestrator/orchestration", - Certificate: ".X509pubKey", + Name: "orchestrator", + Url: "http://localhost:20103/orchestrator/orchestration", } ca := components.CoreSystem{ - Name: "ca", - Url: "http://localhost:20100/ca/certification", - Certificate: ".X509pubKey", + Name: "ca", + Url: "http://localhost:20100/ca/certification", } - coreSystems := []components.CoreSystem{serReg, orches, ca} + maitreD := components.CoreSystem{ + Name: "maitreD", + Url: "http://localhost:20101/maitreD/maitreD", + } + // add the core systems to the configuration file + // the system is part of a local cloud with mandatory core systems + coreSystems := []components.CoreSystem{serReg, orches, ca, maitreD} defaultConfig.CCoreS = coreSystems + var rawBytes []json.RawMessage // the mbaigo library does not know about the unit asset's structure (defined in the file thing.go and not part of the library) + // open the configuration file or create one with the default content prepared above systemConfigFile, err := os.Open("systemconfig.json") if err != nil { // could not find the systemconfig.json so a default one is being created defaultConfigFile, err := os.Create("systemconfig.json") if err != nil { - return rawBytes, servicesList, err + return rawBytes, err } defer defaultConfigFile.Close() - systemconfigjson, err := json.MarshalIndent(defaultConfig, "", " ") + systemconfigjson, err := json.MarshalIndent(defaultConfig, "", " ") if err != nil { - return rawBytes, servicesList, err + return rawBytes, err } nBytes, err := defaultConfigFile.Write(systemconfigjson) if err != nil { - return rawBytes, servicesList, err + return rawBytes, err } - return rawBytes, servicesList, fmt.Errorf("a new configuration file has been written with %d bytes. Please update it and restart the system", nBytes) + return rawBytes, fmt.Errorf("a new configuration file has been written with %d bytes. Please update it and restart the system", nBytes) } // the system configuration file could be open, read the configurations and pass them on to the system defer systemConfigFile.Close() configBytes, err := os.ReadFile("systemconfig.json") if err != nil { - return rawBytes, servicesList, err + return rawBytes, err } // the challenge is that the definition of the unit asset is unknown to the mbaigo library and only known to the system that invokes the library @@ -130,13 +154,13 @@ func Configure(sys *components.System) ([]json.RawMessage, []components.Service, Alias: (*Alias)(&configurationIn), } if err := json.Unmarshal(configBytes, aux); err != nil { - return rawBytes, servicesList, err + return rawBytes, err } if len(aux.Resources) > 0 { configurationIn.rawResources = aux.Resources } else { var rawMessages []json.RawMessage - for _, s := range defaultConfig.UAsset { + for _, s := range defaultConfig.Assets { // convert the struct to JSON-encoded byte array jsonBytes, err := json.Marshal(s) if err != nil { @@ -148,34 +172,13 @@ func Configure(sys *components.System) ([]json.RawMessage, []components.Service, } sys.Name = configurationIn.CName - sys.Husk.DName = configurationIn.PKIdetails sys.Husk.ProtoPort = configurationIn.Protocols for _, ccore := range configurationIn.CCoreS { newCore := ccore sys.CoreS = append(sys.CoreS, &newCore) } - // update the services (e.g., re-registration period, costs, or units) - for i := range configurationIn.CServices { - for _, originalService := range originalSs { - if originalService.Definition == configurationIn.CServices[i].Definition { - configurationIn.CServices[i].Merge(&originalService) // keep the original definition and subpath as the original ones - } - } - } - servicesList = configurationIn.CServices - - return configurationIn.rawResources, servicesList, nil -} - -// getFirstAsset returns the first key-value pair in the Assets map -func getFirstAsset(assetMap map[string]*components.UnitAsset) []components.UnitAsset { - var assetList []components.UnitAsset - for key := range assetMap { - assetList = append(assetList, *assetMap[key]) - return assetList - } - return assetList + return configurationIn.rawResources, nil } // getServicesList() returns the original list of services @@ -187,3 +190,14 @@ func getServicesList(uat components.UnitAsset) []components.Service { } return serviceList } + +// MakeServiceMap() creates a map of services from a slice of services +// The map is indexed by the service subpath +func MakeServiceMap(services []components.Service) map[string]*components.Service { + serviceMap := make(map[string]*components.Service) + for i := range services { + svc := services[i] // take the address of the element in the slice + serviceMap[svc.SubPath] = &svc + } + return serviceMap +} diff --git a/usecases/docs.go b/usecases/docs.go index 412b542..656b149 100644 --- a/usecases/docs.go +++ b/usecases/docs.go @@ -60,17 +60,19 @@ func SysHateoas(w http.ResponseWriter, req *http.Request, sys components.System) w.Write([]byte(resourceURI)) } - text = " having the following services: having the following services:

The services can be accessed using the following protocols with their respective bound ports:

having the following services: having the following services:

The services can be accessed using the following protocols with their respective bound ports: