Skip to content
This repository was archived by the owner on Apr 18, 2024. It is now read-only.

Commit 37ea1fd

Browse files
author
Saul
committed
feat: add client/server integration and build steps
1 parent a35af33 commit 37ea1fd

File tree

11 files changed

+204
-46
lines changed

11 files changed

+204
-46
lines changed

Dockerfile

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ FROM golang:1.16 as builder
22
WORKDIR /app
33
ARG NAME
44
ARG VERSION
5+
RUN apt-get install -y curl \
6+
&& curl -sL https://deb.nodesource.com/setup_16.x | bash - \
7+
&& apt-get install -y nodejs
58
COPY ./ ./
69
RUN make build
710

Makefile

+8-4
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,29 @@ vet:
3333
go vet ./...
3434

3535
.PHONY: build
36-
build:
36+
build: next
3737
go build -ldflags "-X \"main.version=$(VERSION_TAG)\"" -o bin/konfig-manager
3838

3939
.PHONY: install
4040
install: build
4141
cp bin/konfig-manager /usr/local/bin/
4242

43+
.PHONY: next
44+
next:
45+
cd ui && npm ci && npm run export
46+
4347
.PHONY: linux
44-
linux:
48+
linux: next
4549
GOOS=linux GOARCH=amd64 go build -ldflags "-X \"main.version=$(VERSION_TAG)\"" -o .bin/$(NAME)_linux-amd64
4650
GOOS=linux GOARCH=arm64 go build -ldflags "-X \"main.version=$(VERSION_TAG)\"" -o .bin/$(NAME)_linux-arm64
4751

4852
.PHONY: darwin
49-
darwin:
53+
darwin: next
5054
GOOS=darwin GOARCH=amd64 go build -ldflags "-X \"main.version=$(VERSION_TAG)\"" -o .bin/$(NAME)_darwin-amd64
5155
GOOS=darwin GOARCH=arm64 go build -ldflags "-X \"main.version=$(VERSION_TAG)\"" -o .bin/$(NAME)_darwin-arm64
5256

5357
.PHONY: windows
54-
windows:
58+
windows: next
5559
GOOS=windows GOARCH=amd64 go build -o ./.bin/$(NAME).exe -ldflags "-X \"main.version=$(VERSION_TAG)\"" main.go
5660

5761
.PHONY: release

cmd/server.go

+2
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ func init() {
1919
Server.Flags().StringSliceP("repos", "r", []string{}, "list of repos to parse")
2020
Server.Flags().StringSliceP("branches", "b", []string{"main"}, "list of branches to parse in the specified repos")
2121
Server.Flags().StringP("allowed-origins", "", "", "to set the allowed origins in the http server")
22+
Server.Flags().Int("devGuiHttpPort", 3000, "port used by a local npm server in development mode")
23+
Server.Flags().Bool("dev", false, "run in development mode")
2224
}

pkg/api.go

+47
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package pkg
22

33
import (
4+
"errors"
45
"net/http"
6+
"os"
7+
"path/filepath"
58

69
"github.com/flanksource/commons/logger"
710
"k8s.io/apimachinery/pkg/util/json"
@@ -67,6 +70,50 @@ func (input *APIServer) GetConfigHandler() http.HandlerFunc {
6770
}
6871
}
6972

73+
func (input *APIServer) GetSpaHandler(rootFs http.FileSystem) http.HandlerFunc {
74+
readFile := func(file http.File) ([]byte, error) {
75+
fileInfo, _ := file.Stat()
76+
size := fileInfo.Size()
77+
fileBuf := make([]byte, size)
78+
_, err := file.Read(fileBuf)
79+
return fileBuf, err
80+
}
81+
82+
return func(resp http.ResponseWriter, req *http.Request) {
83+
path, err := filepath.Abs(req.URL.Path)
84+
if err != nil {
85+
// if we failed to get the absolute path respond with a 400 bad request
86+
// and stop
87+
http.Error(resp, err.Error(), http.StatusBadRequest)
88+
return
89+
}
90+
_, err = rootFs.Open(path)
91+
if errors.Is(err, os.ErrNotExist) {
92+
// if the path does not return a file or directory, serve back index.html
93+
indexFile, err := rootFs.Open("/index.html")
94+
if err != nil {
95+
http.Error(resp, err.Error(), http.StatusInternalServerError)
96+
return
97+
}
98+
fileData, err := readFile(indexFile)
99+
if err != nil {
100+
http.Error(resp, err.Error(), http.StatusInternalServerError)
101+
return
102+
}
103+
if _, err := resp.Write(fileData); err != nil {
104+
logger.Errorf("failed to write body: %v", err)
105+
}
106+
return
107+
} else if err != nil {
108+
// if we got an error (that wasn't that the file doesn't exist) stating the
109+
// file, return a 500 internal server error and stop
110+
http.Error(resp, err.Error(), http.StatusInternalServerError)
111+
return
112+
}
113+
http.FileServer(rootFs).ServeHTTP(resp, req)
114+
}
115+
}
116+
70117
func removeObjectsFromList(dataWithKustomizeObjects []map[string]KustomizeResources) []map[string]KustomizeResources {
71118
var data []map[string]KustomizeResources
72119
for _, objMap := range dataWithKustomizeObjects {

pkg/server.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,24 @@ package pkg
22

33
import (
44
"fmt"
5+
"io/fs"
56
nethttp "net/http"
67
"path/filepath"
78

89
"github.com/flanksource/commons/logger"
10+
"github.com/flanksource/konfig-manager/ui"
911
"github.com/spf13/cobra"
1012
)
1113

1214
func Server(cmd *cobra.Command) {
15+
var staticRoot nethttp.FileSystem
1316
configFilePath, _ := cmd.Flags().GetString("config-file")
1417
httpPort, _ := cmd.Flags().GetInt("port")
1518
repos, _ := cmd.Flags().GetStringSlice("repos")
1619
branches, _ := cmd.Flags().GetStringSlice("branches")
1720
allowedOrigins, _ := cmd.Flags().GetString("allowed-origins")
21+
devGuiHTTPPort, _ := cmd.Flags().GetInt("devGuiHttpPort")
22+
dev, _ := cmd.Flags().GetBool("dev")
1823

1924
configFilePathAbsPath, err := filepath.Abs(configFilePath)
2025
if err != nil {
@@ -26,9 +31,26 @@ func Server(cmd *cobra.Command) {
2631
ConfigFile: configFilePathAbsPath,
2732
Branches: branches,
2833
}
29-
handler := server.GetConfigHandler()
34+
35+
if dev {
36+
staticRoot = nethttp.Dir("./ui/out")
37+
allowedOrigins = fmt.Sprintf("http://localhost:%d", devGuiHTTPPort)
38+
logger.Infof("Starting in local development mode")
39+
logger.Infof("Allowing access from a GUI on %s", allowedOrigins)
40+
logger.Infof("The GUI can be started with: 'npm run dev' at default port: %d", devGuiHTTPPort)
41+
} else {
42+
fs, err := fs.Sub(ui.StaticContent, "out")
43+
if err != nil {
44+
logger.Errorf("Error: %v", err)
45+
}
46+
staticRoot = nethttp.FS(fs)
47+
}
48+
49+
configHandler := server.GetConfigHandler()
3050
applicationHandler := server.GetApplicationHandler()
31-
nethttp.HandleFunc("/api", simpleCors(handler, allowedOrigins))
51+
spaHandler := server.GetSpaHandler(staticRoot)
52+
nethttp.HandleFunc("/", simpleCors(spaHandler, allowedOrigins))
53+
nethttp.HandleFunc("/api", simpleCors(configHandler, allowedOrigins))
3254
nethttp.HandleFunc("/api/applications", simpleCors(applicationHandler, allowedOrigins))
3355
addr := fmt.Sprintf("0.0.0.0:%d", httpPort)
3456
if err := nethttp.ListenAndServe(addr, nil); err != nil {

ui/next.config.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const productionEnv = {
2+
serverPrefix: "",
3+
};
4+
5+
const developmentEnv = {
6+
serverPrefix: "http://localhost:8080",
7+
};
8+
9+
const env =
10+
process.env.NODE_ENV === "production" ? productionEnv : developmentEnv;
11+
12+
module.exports = {
13+
reactStrictMode: true,
14+
env,
15+
};

ui/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
"main": "index.js",
66
"scripts": {
77
"dev": "next dev",
8+
"export": "NODE_ENV=production && next build && next export",
89
"build": "next build",
910
"start": "next start",
1011
"jest": "jest",
11-
"init": "jest --init",
1212
"lint:eslint": "eslint --ext .js,.jsx,.ts,.tsx .",
1313
"fix:eslint": "eslint --fix --ext .js,.jsx,.ts,.tsx .",
1414
"test": "jest",

ui/pages/[appName].jsx

+57-24
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,37 @@ const App = () => {
3535
"app",
3636
];
3737

38-
const { status, data, error, isFetching } = useQuery(["stuff", appName], () =>
39-
fetchStuff(`http://localhost:3000/test-data/${appName}.json`)
38+
const { status, data, error, isFetching } = useQuery(
39+
["stuff", appName],
40+
() => {
41+
if (window.DEMO_MODE) {
42+
return fetchStuff(
43+
`${process.env.serverPrefix}/test-data/${appName}.json`
44+
);
45+
}
46+
return fetchStuff(
47+
`${process.env.serverPrefix}/api?application=${appName}&objects=true`
48+
);
49+
},
50+
{ staleTime: 1000, refetchOnWindowFocus: false }
4051
);
4152

4253
const {
4354
status: appFetchStatus,
4455
data: apps,
4556
error: appFetchError,
4657
isFetching: isFetchingApps,
47-
} = useQuery(["apps"], () =>
48-
fetchStuff(`http://localhost:3000/test-data/applications.json`)
58+
} = useQuery(
59+
["apps"],
60+
() => {
61+
if (window.DEMO_MODE) {
62+
return fetchStuff(
63+
`${process.env.serverPrefix}/test-data/applications.json`
64+
);
65+
}
66+
return fetchStuff(`${process.env.serverPrefix}/api/applications`);
67+
},
68+
{ staleTime: 1000, refetchOnWindowFocus: false }
4969
);
5070

5171
if (
@@ -81,32 +101,45 @@ const App = () => {
81101
flex-wrap: wrap;
82102
`}
83103
>
84-
{apps.map((app) => (
85-
<Link href={`/${app}`} key={app}>
86-
<li
87-
css={css`
88-
border: 1px solid black;
89-
margin: 1rem;
90-
padding: 1rem;
91-
`}
92-
>
93-
<a href={`/${app}`}>{app}</a>
94-
</li>
95-
</Link>
96-
))}
104+
{Array.isArray(apps) ? (
105+
<ul
106+
css={css`
107+
list-style: none;
108+
display: flex;
109+
flex-wrap: wrap;
110+
`}
111+
>
112+
{apps.map((app) => (
113+
<li
114+
key={app}
115+
css={css`
116+
border: 1px solid black;
117+
margin: 1rem;
118+
padding: 1rem;
119+
`}
120+
>
121+
<Link href={`/${app}`}>
122+
<a href={`/${app}`}>{app}</a>
123+
</Link>
124+
</li>
125+
))}
126+
</ul>
127+
) : (
128+
<div>Application list not found</div>
129+
)}
97130
</ul>
98-
;
99-
<Table
100-
processed={getProcessed(data, hierarchy)}
101-
hierarchy={hierarchy}
102-
appName={appName}
103-
/>
131+
<Table data={data} hierarchy={hierarchy} appName={appName} />
104132
</div>
105133
);
106134
};
107135

108136
const Table = (props) => {
109-
const { processed, hierarchy, appName } = props;
137+
const { data, hierarchy, appName } = props;
138+
139+
const processed = useMemo(
140+
() => getProcessed(data, hierarchy),
141+
[data, hierarchy]
142+
);
110143

111144
const columns = useMemo(
112145
() => [

ui/pages/_app.jsx

+9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import "../styles/globals.css";
22
import { QueryClientProvider, QueryClient } from "react-query";
3+
import { useRouter } from "next/router";
4+
import { useEffect } from "react";
35

46
function App({ Component, pageProps }) {
57
const queryClient = new QueryClient();
68

9+
const router = useRouter();
10+
useEffect(() => {
11+
router.push(window.location.pathname, "", { shallow: true });
12+
// TODO: Decide whether lint exclusion is valid
13+
// eslint-disable-next-line react-hooks/exhaustive-deps
14+
}, []);
15+
716
return (
817
<QueryClientProvider client={queryClient}>
918
<Component {...pageProps} />

ui/pages/index.jsx

+29-15
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,17 @@ export default function Home() {
1919
data: apps,
2020
error,
2121
isFetching,
22-
} = useQuery(["apps"], () =>
23-
fetchStuff(`http://localhost:3000/test-data/applications.json`)
22+
} = useQuery(
23+
["apps"],
24+
() => {
25+
if (window.DEMO_MODE) {
26+
return fetchStuff(
27+
`${process.env.serverPrefix}/test-data/applications.json`
28+
);
29+
}
30+
return fetchStuff(`${process.env.serverPrefix}/api/applications`);
31+
},
32+
{ staleTime: 1000, refetchOnWindowFocus: false }
2433
);
2534

2635
if (status === "loading" || isFetching) {
@@ -40,27 +49,32 @@ export default function Home() {
4049
</Head>
4150

4251
<main className={styles.main}>
43-
<ul
44-
css={css`
45-
list-style: none;
46-
display: flex;
47-
flex-wrap: wrap;
48-
`}
49-
>
50-
{apps.map((app) => (
51-
<Link href={`/${app}`}>
52+
{Array.isArray(apps) ? (
53+
<ul
54+
css={css`
55+
list-style: none;
56+
display: flex;
57+
flex-wrap: wrap;
58+
`}
59+
>
60+
{apps.map((app) => (
5261
<li
62+
key={app}
5363
css={css`
5464
border: 1px solid black;
5565
margin: 1rem;
5666
padding: 1rem;
5767
`}
5868
>
59-
<a href={`/${app}`}>{app}</a>
69+
<Link href={`/${app}`}>
70+
<a href={`/${app}`}>{app}</a>
71+
</Link>
6072
</li>
61-
</Link>
62-
))}
63-
</ul>
73+
))}
74+
</ul>
75+
) : (
76+
<div>Application list not found</div>
77+
)}
6478
</main>
6579
</div>
6680
);

ui/static.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package ui
2+
3+
import "embed"
4+
5+
//nolint
6+
//go:embed out/*
7+
//go:embed out/_next/static/*/_*
8+
//go:embed out/_next/static/chunks/*/_*
9+
var StaticContent embed.FS

0 commit comments

Comments
 (0)