Compare commits

...
7 Commits
4 changed files with 31 additions and 23 deletions
+6 -9
View File
@@ -13,7 +13,7 @@ Unlike traditional orchestrators, there's no central control plane and quorum to
synchronized copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if synchronized copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
some machines go offline. some machines go offline.
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the Uncloud is the solution for developers who want the flexibility of self-hosted infrastructure without the
operational complexity of Kubernetes. operational complexity of Kubernetes.
## 🎬 Quick demo ## 🎬 Quick demo
@@ -46,12 +46,6 @@ Deploy a highly available web app with automatic HTTPS across multiple regions a
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications. * **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster. * **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
Coming soon:
* Project isolation through environments/namespaces
* Configs and secrets management using providers such as HashiCorp Vault and 1Password
* Monitoring and log aggregation
Here is a diagram of an Uncloud multi-provider cluster of 3 machines: Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
![Diagram: multi-provider cluster of 3 machines](website/images/diagram.webp) ![Diagram: multi-provider cluster of 3 machines](website/images/diagram.webp)
@@ -120,8 +114,10 @@ View the [user guide](docs/user_guide.md) for more information.
## ⚙️ How it works ## ⚙️ How it works
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals.
under the hood to see what happens when you run certain commands.
<details>
<summary>Here, let's peek under the hood to see what happens when you run certain commands.</summary>
**When you initialize a new cluster on a machine:** **When you initialize a new cluster on a machine:**
@@ -256,6 +252,7 @@ Look ma, no control plane or master nodes to maintain! Just a simple overlay net
sync that lets machines work together. Want to check on things or make changes? Connect to any machine either implicitly sync that lets machines work together. Want to check on things or make changes? Connect to any machine either implicitly
using the CLI or directly over SSH. They all have the complete cluster state and can control everything. It's like each using the CLI or directly over SSH. They all have the complete cluster state and can control everything. It's like each
machine is a full backup of your control plane. machine is a full backup of your control plane.
</details>
## 🏗 Project status ## 🏗 Project status
+9 -7
View File
@@ -152,7 +152,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
} }
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error { func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
if _, err := clusterClient.GetDomain(ctx); err != nil { domain, err := clusterClient.GetDomain(ctx)
if err != nil {
if errors.Is(err, api.ErrNotFound) { if errors.Is(err, api.ErrNotFound) {
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').") fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
return nil return nil
@@ -164,7 +165,7 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation. // TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
var records []*pb.DNSRecord var records []*pb.DNSRecord
err := progress.RunWithTitle(ctx, func(ctx context.Context) error { err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
var err error var err error
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName) records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
return err return err
@@ -172,18 +173,19 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
if err != nil { if err != nil {
if errors.Is(err, client.ErrNoReachableMachines) { if errors.Is(err, client.ErrNoReachableMachines) {
fmt.Println() fmt.Println()
fmt.Println("DNS records could not be updated as there are no internet-reachable machines running " + fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
"caddy containers.") "machines running caddy containers.\n", domain)
fmt.Println() fmt.Println()
fmt.Println("Possible solutions:") fmt.Println("Possible solutions:")
fmt.Println("- Ensure your machines have public IP addresses") fmt.Println("- Ensure your machines have public IP addresses")
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs") fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs")
fmt.Println("- Check firewall settings on your machines") fmt.Println("- Check firewall settings on your machines")
fmt.Println("- Configure port forwarding if behind NAT") fmt.Println("- Configure port forwarding if behind NAT")
fmt.Println("- Retry Caddy deployment after resolving connectivity issues with 'uc caddy deploy'") fmt.Println("- Retry Caddy deployment with 'uc caddy deploy' after resolving connectivity issues")
fmt.Println() fmt.Println()
fmt.Println("Your services will not be accessible from the internet until at least one machine " + fmt.Println("Your services won't be accessible from the internet until at least one machine " +
"becomes reachable.") "becomes reachable. If you aren't planning to expose any services publicly, you can release " +
"the domain by running 'uc dns release'.")
} }
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err) return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
} }
+11 -2
View File
@@ -31,6 +31,7 @@ const (
) )
var serviceIDRegexp = regexp.MustCompile("^[0-9a-f]{32}$") var serviceIDRegexp = regexp.MustCompile("^[0-9a-f]{32}$")
var dnsLabelRegexp = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
func ValidateServiceID(id string) bool { func ValidateServiceID(id string) bool {
return serviceIDRegexp.MatchString(id) return serviceIDRegexp.MatchString(id)
@@ -103,8 +104,15 @@ func (s *ServiceSpec) Validate() error {
default: default:
return fmt.Errorf("invalid mode: %q", s.Mode) return fmt.Errorf("invalid mode: %q", s.Mode)
} }
// TODO: validate the service name is a valid DNS label. if s.Name != "" {
if len(s.Name) > 63 {
return fmt.Errorf("service name too long (max 63 characters): %q", s.Name)
}
if !dnsLabelRegexp.MatchString(s.Name) {
return fmt.Errorf("invalid service name: %q. must be 1-63 characters, lowercase letters, numbers, and dashes only; must start and end with a letter or number", s.Name)
}
}
for _, p := range s.Ports { for _, p := range s.Ports {
if (p.Mode == "" || p.Mode == PortModeIngress) && if (p.Mode == "" || p.Mode == PortModeIngress) &&
@@ -138,6 +146,7 @@ func (s *ServiceSpec) Validate() error {
return nil return nil
} }
func (s *ServiceSpec) Clone() ServiceSpec { func (s *ServiceSpec) Clone() ServiceSpec {
spec := *s spec := *s
+5 -5
View File
@@ -24,12 +24,12 @@ var caddyImageTagRegex = regexp.MustCompile(`^2\.\d+\.\d+$`)
// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest // The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest
// version of the official Caddy Docker image is used. // version of the official Caddy Docker image is used.
func (cli *Client) NewCaddyDeployment(image string, placement api.Placement) (*deploy.Deployment, error) { func (cli *Client) NewCaddyDeployment(image string, placement api.Placement) (*deploy.Deployment, error) {
latest, err := LatestCaddyImage()
if err != nil {
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
}
if image == "" { if image == "" {
latest, err := LatestCaddyImage()
if err != nil {
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
}
image = reference.FamiliarString(latest) image = reference.FamiliarString(latest)
} }