Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add config converter #180

Merged
merged 5 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,44 @@ concurrency:
group: "pages"
cancel-in-progress: false

permissions:
contents: read
packages: write

jobs:
# Single deploy job since we're just deploying
deploy:
publish-backend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version-file: "backend/go.mod"
cache: true
- uses: ko-build/[email protected]
with:
version: v0.15.1
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare
run: |
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: Build and push
working-directory: backend
run: |
export KO_DOCKER_REPO=ghcr.io/grafana/agent-configurator-backend
ko build --sbom=none --bare --platform linux/arm64,linux/arm/v7,linux/amd64 -t ${{ github.ref_name } \
--image-label org.opencontainers.image.title=agent-configurator-backend \
--image-label org.opencontainers.image.description="Backend for the agent-configurator" \
--image-label org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }} \
--image-label org.opencontainers.image.revision=${{ github.sha }} \
--image-label org.opencontainers.image.version=${{ github.ref_name }} \
--image-label org.opencontainers.image.created=${{ env.BUILD_DATE }}
deploy-frontend:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
Expand Down
640 changes: 640 additions & 0 deletions backend/go.mod

Large diffs are not rendered by default.

2,779 changes: 2,779 additions & 0 deletions backend/go.sum

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions backend/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"time"

"github.com/grafana/agent/converter"
"github.com/grafana/agent/converter/diag"
"github.com/rs/cors"
)

type ConversionRequest struct {
Data string `json:"data"`
Type converter.Input `json:"type"`
}

type ConversionResponse struct {
Data string `json:"data"`
Diagnostics diag.Diagnostics `json:"diagnostics"`
}

func convert(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
slog.InfoContext(r.Context(), "finished conversion request", "duration", time.Since(start))
}()

if r.Method != http.MethodPost {
http.Error(w, "method not supported", http.StatusBadRequest)
return
}

var cr ConversionRequest
if err := json.NewDecoder(r.Body).Decode(&cr); err != nil {
http.Error(w, fmt.Sprintf("decoding request: %s", err.Error()), http.StatusBadRequest)
return
}
out, diag := converter.Convert([]byte(cr.Data), cr.Type, nil)
json.NewEncoder(w).Encode(ConversionResponse{
Data: string(out),
Diagnostics: diag,
})
}

func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8080"
}
mux := http.NewServeMux()
mux.HandleFunc("/convert", convert)
cmux := cors.Default().Handler(mux)
slog.Info("starting server", "address", addr)
http.ListenAndServe(addr, cmux)
}
Binary file modified public/tree-sitter-river.wasm
Binary file not shown.
26 changes: 24 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Tooltip,
VerticalGroup,
HorizontalGroup,
Badge,
} from "@grafana/ui";
import Header from "./components/Header";
import ConfigEditor from "./components/ConfigEditor";
Expand All @@ -19,6 +20,7 @@ import ExamplesCatalog from "./components/ExamplesCatalog";
import { useModelContext } from "./state";
import InstallationInstructions from "./components/InstallationInstructions";
import ConfigurationWizard from "./components/ConfigurationWizard";
import Converter from "./components/Converter";

function App() {
const styles = useStyles(getStyles);
Expand All @@ -28,9 +30,14 @@ function App() {
const [examplesCatalogOpen, setExamplesCatalogOpen] = useState(false);
const openExamples = () => setExamplesCatalogOpen(true);
const closeExamples = () => setExamplesCatalogOpen(false);
const [converterOpen, setConverterOpen] = useState(false);
const openConverter = () => setConverterOpen(true);
const closeConverter = () => setConverterOpen(false);
const { model } = useModelContext();
const [copied, setCopied] = useState(false);

const converterEnabled = !!process.env.REACT_APP_CONVERT_ENDPOINT;

const shareLink = useMemo(
() => `${window.location}?c=${btoa(model)}`,
[model],
Expand Down Expand Up @@ -63,10 +70,18 @@ function App() {
configuration, based on your usecase.
</p>
<HorizontalGroup>
<Button onClick={openWizard} variant="success">
<Button onClick={openWizard} variant="primary">
Start configuration wizard
</Button>
<Button onClick={openExamples}>Open examples catalog</Button>
{converterEnabled && (
<Button onClick={openConverter} variant="secondary">
<Badge text="New" icon="rocket" color="green" />
Convert your existing configuration
</Button>
)}
<Button onClick={openExamples} variant="secondary">
Open examples catalog
</Button>
<LinkButton
variant="secondary"
href="https://grafana.com/docs/agent/latest/flow/"
Expand Down Expand Up @@ -127,6 +142,13 @@ function App() {
>
<ExamplesCatalog dismiss={closeExamples} />
</Modal>
<Modal
title="Configuration Converter"
isOpen={converterOpen}
onDismiss={closeConverter}
>
<Converter dismiss={closeConverter} />
</Modal>
</div>
);
}
Expand Down
178 changes: 178 additions & 0 deletions src/components/Converter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {
Alert,
AlertVariant,
Button,
Field,
FieldSet,
Modal,
Select,
TextArea,
} from "@grafana/ui";
import { css } from "@emotion/css";
import { useModelContext } from "../../state";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { useMemo, useState } from "react";

type InputType = "prometheus" | "promtail" | "static";

interface Diagnostic {
Severity: number;
Summary: string;
Detail: string;
}

interface ConversionRequest {
data: string;
type: InputType;
}
interface ConversionResponse {
data?: string;
diagnostics: Diagnostic[];
}

const severities: AlertVariant[] = [
"info",
"info",
"warning",
"error",
"error",
];

const convertConfig = async (
r: ConversionRequest,
): Promise<ConversionResponse> => {
const resp = await fetch(process.env.REACT_APP_CONVERT_ENDPOINT as string, {
method: "POST",
body: JSON.stringify(r),
});
if (resp.status !== 200) {
return {
diagnostics: [
{
Severity: 1,
Summary: "failed to convert: " + (await resp.text()),
Detail: "",
},
],
};
}
const rj = await resp.json();
return rj;
};

const Converter = ({ dismiss }: { dismiss: () => void }) => {
const { setModel } = useModelContext();

const [diags, setDiags] = useState<Diagnostic[]>([]);
const hasDiagnostics = useMemo(() => {
return diags.length > 0;
}, [diags]);
const [converted, setConverted] = useState<string | null>(null);

const defaultValues = {
data: "",
type: "static" as InputType,
};

const formAPI = useForm<ConversionRequest>({
mode: "onSubmit",
defaultValues: defaultValues,
shouldFocusError: true,
});
const { register, control, handleSubmit } = formAPI;

return (
<>
{!hasDiagnostics && (
<FormProvider {...formAPI}>
<form onSubmit={(e) => e.preventDefault()}>
<FieldSet>
<Field label="Source Type">
<Controller
control={control}
name="type"
render={({ field: { ref, ...f } }) => (
<Select
value={f.value}
onChange={(s) => f.onChange(s.value)}
options={[
{
label: "Grafana Agent Static",
value: "static",
},
{
label: "Prometheus",
value: "prometheus",
},
{
label: "Promtail",
value: "promtail",
},
]}
/>
)}
rules={{ required: true }}
/>
</Field>
<Field label="Source configuration">
<TextArea rows={20} {...register("data")} />
</Field>
</FieldSet>
<Modal.ButtonRow>
<Button
type="button"
onClick={handleSubmit(async (values) => {
const resp = await convertConfig(values);
setDiags(resp.diagnostics);
if (resp.data) setConverted(resp.data);
})}
>
Next
</Button>
</Modal.ButtonRow>
</form>
</FormProvider>
)}
{hasDiagnostics && (
<>
{diags.map((d, i) => (
<Alert key={i} title={d.Summary} severity={severities[d.Severity]}>
{d.Detail && (
<p
className={css`
white-space: pre-line;
margin: 0;
`}
>
{d.Detail}
</p>
)}
</Alert>
))}
<Modal.ButtonRow>
<Button
type="button"
onClick={() => setDiags([])}
variant="secondary"
>
Back
</Button>
{converted != null && (
<Button
type="button"
onClick={() => {
setModel(converted);
dismiss();
}}
>
Apply
</Button>
)}
</Modal.ButtonRow>
</>
)}
</>
);
};

export default Converter;