Skip to content

Commit 3c23629

Browse files
committed
Add ingress command
1 parent 53ffd8a commit 3c23629

File tree

2 files changed

+260
-0
lines changed

2 files changed

+260
-0
lines changed

pkg/cmd/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func init() {
7474
&psCmd,
7575
&logsCmd,
7676
&rmCmd,
77+
&ingressCmd,
7778
{
7879
Name: "health",
7980
Category: "API RESOURCE",

pkg/cmd/ingresscmd.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"strings"
9+
10+
"github.com/onkernel/hypeman-go"
11+
"github.com/onkernel/hypeman-go/option"
12+
"github.com/urfave/cli/v3"
13+
)
14+
15+
var ingressCmd = cli.Command{
16+
Name: "ingress",
17+
Usage: "Manage ingresses",
18+
Commands: []*cli.Command{
19+
&ingressCreateCmd,
20+
&ingressListCmd,
21+
&ingressDeleteCmd,
22+
},
23+
HideHelpCommand: true,
24+
}
25+
26+
var ingressCreateCmd = cli.Command{
27+
Name: "create",
28+
Usage: "Create an ingress for an instance",
29+
ArgsUsage: "<instance>",
30+
Flags: []cli.Flag{
31+
&cli.StringFlag{
32+
Name: "hostname",
33+
Aliases: []string{"H"},
34+
Usage: "Hostname to match (exact match on Host header)",
35+
Required: true,
36+
},
37+
&cli.IntFlag{
38+
Name: "port",
39+
Aliases: []string{"p"},
40+
Usage: "Target port on the instance",
41+
Required: true,
42+
},
43+
&cli.IntFlag{
44+
Name: "host-port",
45+
Usage: "Host port to listen on (default: 80)",
46+
Value: 80,
47+
},
48+
&cli.BoolFlag{
49+
Name: "tls",
50+
Usage: "Enable TLS termination (certificate auto-issued via ACME)",
51+
},
52+
&cli.BoolFlag{
53+
Name: "redirect-http",
54+
Usage: "Auto-create HTTP to HTTPS redirect (only applies when --tls is enabled)",
55+
},
56+
&cli.StringFlag{
57+
Name: "name",
58+
Usage: "Ingress name (auto-generated from hostname if not provided)",
59+
},
60+
},
61+
Action: handleIngressCreate,
62+
HideHelpCommand: true,
63+
}
64+
65+
var ingressListCmd = cli.Command{
66+
Name: "list",
67+
Usage: "List ingresses",
68+
Flags: []cli.Flag{
69+
&cli.BoolFlag{
70+
Name: "quiet",
71+
Aliases: []string{"q"},
72+
Usage: "Only display ingress IDs",
73+
},
74+
},
75+
Action: handleIngressList,
76+
HideHelpCommand: true,
77+
}
78+
79+
var ingressDeleteCmd = cli.Command{
80+
Name: "delete",
81+
Usage: "Delete an ingress",
82+
ArgsUsage: "<id>",
83+
Action: handleIngressDelete,
84+
HideHelpCommand: true,
85+
}
86+
87+
// ingressRule is a custom type to include TLS fields not yet in the SDK
88+
type ingressRule struct {
89+
Match ingressMatch `json:"match"`
90+
Target ingressTarget `json:"target"`
91+
TLS bool `json:"tls,omitempty"`
92+
RedirectHTTP bool `json:"redirect_http,omitempty"`
93+
}
94+
95+
type ingressMatch struct {
96+
Hostname string `json:"hostname"`
97+
Port int64 `json:"port,omitempty"`
98+
}
99+
100+
type ingressTarget struct {
101+
Instance string `json:"instance"`
102+
Port int64 `json:"port"`
103+
}
104+
105+
type ingressCreateRequest struct {
106+
Name string `json:"name"`
107+
Rules []ingressRule `json:"rules"`
108+
}
109+
110+
func handleIngressCreate(ctx context.Context, cmd *cli.Command) error {
111+
args := cmd.Args().Slice()
112+
if len(args) < 1 {
113+
return fmt.Errorf("instance name or ID required\nUsage: hypeman ingress create <instance> --hostname <hostname> --port <port>")
114+
}
115+
116+
instance := args[0]
117+
hostname := cmd.String("hostname")
118+
port := cmd.Int("port")
119+
hostPort := cmd.Int("host-port")
120+
tls := cmd.Bool("tls")
121+
redirectHTTP := cmd.Bool("redirect-http")
122+
name := cmd.String("name")
123+
124+
// Auto-generate name from hostname if not provided
125+
if name == "" {
126+
name = generateIngressName(hostname)
127+
}
128+
129+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
130+
131+
var opts []option.RequestOption
132+
if cmd.Root().Bool("debug") {
133+
opts = append(opts, debugMiddlewareOption)
134+
}
135+
136+
// Build custom request body to include TLS fields
137+
reqBody := ingressCreateRequest{
138+
Name: name,
139+
Rules: []ingressRule{
140+
{
141+
Match: ingressMatch{
142+
Hostname: hostname,
143+
Port: int64(hostPort),
144+
},
145+
Target: ingressTarget{
146+
Instance: instance,
147+
Port: int64(port),
148+
},
149+
TLS: tls,
150+
RedirectHTTP: redirectHTTP,
151+
},
152+
},
153+
}
154+
155+
bodyBytes, err := json.Marshal(reqBody)
156+
if err != nil {
157+
return fmt.Errorf("failed to marshal request: %w", err)
158+
}
159+
160+
opts = append(opts, option.WithRequestBody("application/json", bodyBytes))
161+
162+
fmt.Fprintf(os.Stderr, "Creating ingress %s...\n", name)
163+
164+
result, err := client.Ingresses.New(ctx, hypeman.IngressNewParams{}, opts...)
165+
if err != nil {
166+
return err
167+
}
168+
169+
fmt.Println(result.ID)
170+
return nil
171+
}
172+
173+
func handleIngressList(ctx context.Context, cmd *cli.Command) error {
174+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
175+
176+
var opts []option.RequestOption
177+
if cmd.Root().Bool("debug") {
178+
opts = append(opts, debugMiddlewareOption)
179+
}
180+
181+
ingresses, err := client.Ingresses.List(ctx, opts...)
182+
if err != nil {
183+
return err
184+
}
185+
186+
quietMode := cmd.Bool("quiet")
187+
188+
if quietMode {
189+
for _, ing := range *ingresses {
190+
fmt.Println(ing.ID)
191+
}
192+
return nil
193+
}
194+
195+
if len(*ingresses) == 0 {
196+
fmt.Fprintln(os.Stderr, "No ingresses found.")
197+
return nil
198+
}
199+
200+
table := NewTableWriter(os.Stdout, "ID", "NAME", "HOSTNAME", "TARGET", "TLS", "CREATED")
201+
for _, ing := range *ingresses {
202+
// Extract first rule's hostname and target for display
203+
hostname := ""
204+
target := ""
205+
tlsEnabled := "-"
206+
if len(ing.Rules) > 0 {
207+
rule := ing.Rules[0]
208+
hostname = rule.Match.Hostname
209+
target = fmt.Sprintf("%s:%d", rule.Target.Instance, rule.Target.Port)
210+
}
211+
212+
table.AddRow(
213+
TruncateID(ing.ID),
214+
TruncateString(ing.Name, 20),
215+
TruncateString(hostname, 25),
216+
target,
217+
tlsEnabled,
218+
FormatTimeAgo(ing.CreatedAt),
219+
)
220+
}
221+
table.Render()
222+
223+
return nil
224+
}
225+
226+
func handleIngressDelete(ctx context.Context, cmd *cli.Command) error {
227+
args := cmd.Args().Slice()
228+
if len(args) < 1 {
229+
return fmt.Errorf("ingress ID or name required\nUsage: hypeman ingress delete <id>")
230+
}
231+
232+
id := args[0]
233+
234+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
235+
236+
var opts []option.RequestOption
237+
if cmd.Root().Bool("debug") {
238+
opts = append(opts, debugMiddlewareOption)
239+
}
240+
241+
err := client.Ingresses.Delete(ctx, id, opts...)
242+
if err != nil {
243+
return err
244+
}
245+
246+
fmt.Fprintf(os.Stderr, "Ingress %s deleted.\n", id)
247+
return nil
248+
}
249+
250+
// generateIngressName generates an ingress name from hostname
251+
func generateIngressName(hostname string) string {
252+
// Replace dots with dashes, remove invalid chars
253+
name := strings.ReplaceAll(hostname, ".", "-")
254+
name = strings.ToLower(name)
255+
256+
// Add random suffix
257+
suffix := randomSuffix(4)
258+
return fmt.Sprintf("%s-%s", name, suffix)
259+
}

0 commit comments

Comments
 (0)