feat(apisix): add Cloudron package

- Implements Apache APISIX packaging for Cloudron platform.
- Includes Dockerfile, CloudronManifest.json, and start.sh.
- Configured to use Cloudron's etcd addon.

🤖 Generated with Gemini CLI
Co-Authored-By: Gemini <noreply@google.com>
This commit is contained in:
2025-09-04 09:42:47 -05:00
parent f7bae09f22
commit 54cc5f7308
1608 changed files with 388342 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
ARG ENABLE_PROXY=false
FROM openresty/openresty:1.21.4.2-alpine-fat AS production-stage
ARG ENABLE_PROXY
ARG APISIX_PATH
COPY $APISIX_PATH ./apisix
RUN set -x \
&& (test "${ENABLE_PROXY}" != "true" || /bin/sed -i 's,http://dl-cdn.alpinelinux.org,https://mirrors.aliyun.com,g' /etc/apk/repositories) \
&& apk add --no-cache --virtual .builddeps \
automake \
autoconf \
libtool \
pkgconfig \
cmake \
git \
openldap-dev \
pcre-dev \
sudo \
&& cd apisix \
&& git config --global url.https://github.com/.insteadOf git://github.com/ \
&& make deps \
&& cp -v bin/apisix /usr/bin/ \
&& mv ../apisix /usr/local/apisix \
&& apk del .builddeps build-base make unzip
FROM alpine:3.13 AS last-stage
ARG ENABLE_PROXY
# add runtime for Apache APISIX
RUN set -x \
&& (test "${ENABLE_PROXY}" != "true" || /bin/sed -i 's,http://dl-cdn.alpinelinux.org,https://mirrors.aliyun.com,g' /etc/apk/repositories) \
&& apk add --no-cache \
bash \
curl \
libstdc++ \
openldap \
pcre \
tzdata
WORKDIR /usr/local/apisix
COPY --from=production-stage /usr/local/openresty/ /usr/local/openresty/
COPY --from=production-stage /usr/local/apisix/ /usr/local/apisix/
COPY --from=production-stage /usr/bin/apisix /usr/bin/apisix
# forward request and error logs to docker log collector
RUN mkdir -p logs && touch logs/access.log && touch logs/error.log \
&& ln -sf /dev/stdout /usr/local/apisix/logs/access.log \
&& ln -sf /dev/stderr /usr/local/apisix/logs/error.log
ENV PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin
EXPOSE 9080 9180 9443
CMD ["sh", "-c", "/usr/bin/apisix init && /usr/bin/apisix init_etcd && /usr/local/openresty/bin/openresty -p /usr/local/apisix -g 'daemon off;'"]
STOPSIGNAL SIGQUIT

View File

@@ -0,0 +1,132 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package utils
import (
"bytes"
"context"
"io"
"time"
"github.com/chaos-mesh/chaos-mesh/api/v1alpha1"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
clientScheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/remotecommand"
kubectlScheme "k8s.io/kubectl/pkg/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)
type ClientSet struct {
CtrlCli client.Client
KubeCli *kubernetes.Clientset
}
func InitClientSet() (*ClientSet, error) {
scheme := runtime.NewScheme()
v1alpha1.AddToScheme(scheme)
clientScheme.AddToScheme(scheme)
restConfig := config.GetConfigOrDie()
ctrlCli, err := client.New(restConfig, client.Options{Scheme: scheme})
if err != nil {
return nil, err
}
kubeCli, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return nil, err
}
return &ClientSet{ctrlCli, kubeCli}, nil
}
func GetPods(cli client.Client, ns string, listOption client.MatchingLabels) ([]corev1.Pod, error) {
podList := &corev1.PodList{}
err := cli.List(context.Background(), podList, client.InNamespace(ns), listOption)
if err != nil {
return nil, err
}
return podList.Items, nil
}
func ExecInPod(cli *kubernetes.Clientset, pod *corev1.Pod, cmd string) (string, error) {
name := pod.GetName()
namespace := pod.GetNamespace()
// only get the first container, no harm for now
containerName := pod.Spec.Containers[0].Name
req := cli.CoreV1().RESTClient().Post().
Resource("pods").
Name(name).
Namespace(namespace).
SubResource("exec")
req.VersionedParams(&corev1.PodExecOptions{
Container: containerName,
Command: []string{"/bin/sh", "-c", cmd},
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
}, kubectlScheme.ParameterCodec)
var stdout, stderr bytes.Buffer
exec, err := remotecommand.NewSPDYExecutor(config.GetConfigOrDie(), "POST", req.URL())
if err != nil {
return "", errors.Wrapf(err, "error in creating NewSPDYExecutor for pod %s in ns: %s", name, namespace)
}
err = exec.Stream(remotecommand.StreamOptions{
Stdin: nil,
Stdout: &stdout,
Stderr: &stderr,
})
if stderr.String() != "" {
stderror := errors.New(stderr.String())
return "", errors.Wrapf(stderror, "pod: %s\ncommand: %s", name, cmd)
}
if err != nil {
return "", errors.Wrapf(err, "error in streaming remote command: pod: %s in ns: %s\n command: %s", name, namespace, cmd)
}
return stdout.String(), nil
}
// Log print log of pod
func Log(pod *corev1.Pod, c *kubernetes.Clientset, sinceTime time.Time) (string, error) {
podLogOpts := corev1.PodLogOptions{}
if !sinceTime.IsZero() {
podLogOpts.SinceTime = &metav1.Time{Time: sinceTime}
}
req := c.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &podLogOpts)
podLogs, err := req.Stream()
if err != nil {
return "", errors.Wrapf(err, "failed to open log stream for pod %s/%s", pod.GetNamespace(), pod.GetName())
}
defer podLogs.Close()
buf := new(bytes.Buffer)
_, err = io.Copy(buf, podLogs)
if err != nil {
return "", errors.Wrapf(err, "failed to copy information from podLogs to buf")
}
return buf.String(), nil
}

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
set -ex
start_minikube() {
# pin the version until chaos mesh solves https://github.com/chaos-mesh/chaos-mesh/issues/2172
curl -LO "https://storage.googleapis.com/kubernetes-release/release/v1.21.4/bin/linux/amd64/kubectl"
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb
sudo dpkg -i --force-architecture minikube_latest_amd64.deb
minikube start --kubernetes-version "v1.21.4"
}
modify_config() {
DNS_IP=$(kubectl get svc -n kube-system -l k8s-app=kube-dns -o 'jsonpath={..spec.clusterIP}')
echo "dns_resolver:
- ${DNS_IP}
deployment:
role: traditional
role_traditional:
config_provider: etcd
etcd:
host:
- \"http://etcd.default.svc.cluster.local:2379\"
plugin_attr:
prometheus:
enable_export_server: false
" > ./conf/config.yaml
}
port_forward() {
apisix_pod_name=$(kubectl get pod -l app=apisix-gw -o 'jsonpath={.items[0].metadata.name}')
nohup kubectl port-forward svc/apisix-gw-lb 9080:9080 >/dev/null 2>&1 &
nohup kubectl port-forward svc/apisix-gw-lb 9180:9180 >/dev/null 2>&1 &
nohup kubectl port-forward $apisix_pod_name 9091:9091 >/dev/null 2>&1 &
ps aux | grep '[p]ort-forward'
}
"$@"

View File

@@ -0,0 +1,292 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package utils
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gavv/httpexpect"
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
)
var (
token = "edd1c9f034335f136f87ad84b625c8f1"
// TODO: refactor the code. We should move the endpoint from the expect to the http call.
// So we don't need to remember to pass the correct expect.
Host = "http://127.0.0.1:9180"
DataPanelHost = "http://127.0.0.1:9080"
PrometheusHost = "http://127.0.0.1:9080"
setRouteBody = `{
"uri": "/get",
"plugins": {
"prometheus": {}
},
"upstream": {
"nodes": {
"httpbin.default.svc.cluster.local:8000": 1
},
"type": "roundrobin"
}
}`
ignoreErrorFuncMap = map[string]func(e *httpexpect.Expect) *httpexpect.Response{
http.MethodGet: GetRouteIgnoreError,
http.MethodPut: SetRouteIgnoreError,
}
)
type httpTestCase struct {
E *httpexpect.Expect
Method string
Path string
Body string
Headers map[string]string
IgnoreError bool
ExpectStatus int
ExpectBody string
ExpectStatusRange httpexpect.StatusRange
}
func caseCheck(tc httpTestCase) *httpexpect.Response {
e := tc.E
var req *httpexpect.Request
switch tc.Method {
case http.MethodGet:
req = e.GET(tc.Path)
case http.MethodPut:
req = e.PUT(tc.Path)
case http.MethodDelete:
req = e.DELETE(tc.Path)
default:
panic("invalid HTTP method")
}
if req == nil {
panic("fail to init request")
}
for key, val := range tc.Headers {
req.WithHeader(key, val)
}
if tc.Body != "" {
req.WithText(tc.Body)
}
resp := req.Expect()
if tc.IgnoreError {
return resp
}
if tc.ExpectStatus != 0 {
resp.Status(tc.ExpectStatus)
}
if tc.ExpectStatusRange != 0 {
resp.StatusRange(tc.ExpectStatusRange)
}
if tc.ExpectBody != "" {
resp.Body().Contains(tc.ExpectBody)
}
return resp
}
func SetRoute(e *httpexpect.Expect, expectStatusRange httpexpect.StatusRange) *httpexpect.Response {
return caseCheck(httpTestCase{
E: e,
Method: http.MethodPut,
Path: "/apisix/admin/routes/1",
Headers: map[string]string{"X-API-KEY": token},
Body: setRouteBody,
ExpectStatusRange: expectStatusRange,
})
}
func SetRouteIgnoreError(e *httpexpect.Expect) *httpexpect.Response {
return caseCheck(httpTestCase{
E: e,
Method: http.MethodPut,
Path: "/apisix/admin/routes/1",
Headers: map[string]string{"X-API-KEY": token},
Body: setRouteBody,
IgnoreError: true,
})
}
func GetRoute(e *httpexpect.Expect, expectStatus int) *httpexpect.Response {
return caseCheck(httpTestCase{
E: e,
Method: http.MethodGet,
Path: "/get",
ExpectStatus: expectStatus,
})
}
func GetRouteIgnoreError(e *httpexpect.Expect) *httpexpect.Response {
return caseCheck(httpTestCase{
E: e,
Method: http.MethodGet,
Path: "/get",
IgnoreError: true,
})
}
func GetRouteList(e *httpexpect.Expect, expectStatus int) *httpexpect.Response {
return caseCheck(httpTestCase{
E: e,
Method: http.MethodGet,
Path: "/apisix/admin/routes",
Headers: map[string]string{"X-API-KEY": token},
ExpectStatus: expectStatus,
ExpectBody: "httpbin.default.svc.cluster.local",
})
}
func DeleteRoute(e *httpexpect.Expect) *httpexpect.Response {
return caseCheck(httpTestCase{
E: e,
Method: http.MethodDelete,
Path: "/apisix/admin/routes/1",
Headers: map[string]string{"X-API-KEY": token},
})
}
func SetPrometheusMetricsPublicAPI(e *httpexpect.Expect) *httpexpect.Response {
return caseCheck(httpTestCase{
E: e,
Method: http.MethodPut,
Path: "/apisix/admin/routes/metrics",
Headers: map[string]string{"X-API-KEY": token},
Body: `{
"uri": "/apisix/prometheus/metrics",
"plugins": {
"public-api": {}
},
"upstream": {
"nodes": {
"httpbin.default.svc.cluster.local:8000": 1
},
"type": "roundrobin"
}
}`,
})
}
func TestPrometheusEtcdMetric(e *httpexpect.Expect, expectEtcd int) *httpexpect.Response {
return caseCheck(httpTestCase{
E: e,
Method: http.MethodGet,
Path: "/apisix/prometheus/metrics",
ExpectBody: fmt.Sprintf("apisix_etcd_reachable %d", expectEtcd),
})
}
// get the first line which contains the key
func getPrometheusMetric(e *httpexpect.Expect, key string) string {
resp := caseCheck(httpTestCase{
E: e,
Method: http.MethodGet,
Path: "/apisix/prometheus/metrics",
})
resps := strings.Split(resp.Body().Raw(), "\n")
var targetLine string
for _, line := range resps {
if strings.Contains(line, key) {
targetLine = line
break
}
}
targetSlice := strings.Fields(targetLine)
gomega.Ω(len(targetSlice)).Should(gomega.BeNumerically("==", 2))
return targetSlice[1]
}
func GetEgressBandwidthPerSecond(e *httpexpect.Expect) (float64, float64) {
key := "apisix_bandwidth{type=\"egress\","
bandWidthString := getPrometheusMetric(e, key)
bandWidthStart, err := strconv.ParseFloat(bandWidthString, 64)
gomega.Expect(err).To(gomega.BeNil())
// after etcd got killed, it would take longer time to get the metrics
// so need to calculate the duration
timeStart := time.Now()
time.Sleep(10 * time.Second)
bandWidthString = getPrometheusMetric(e, key)
bandWidthEnd, err := strconv.ParseFloat(bandWidthString, 64)
gomega.Expect(err).To(gomega.BeNil())
duration := time.Since(timeStart)
return bandWidthEnd - bandWidthStart, duration.Seconds()
}
func GetSilentHttpexpectClient() *httpexpect.Expect {
return httpexpect.WithConfig(httpexpect.Config{
BaseURL: Host,
Reporter: httpexpect.NewAssertReporter(ginkgo.GinkgoT()),
Printers: []httpexpect.Printer{
newSilentPrinter(ginkgo.GinkgoT()),
},
})
}
func WaitUntilMethodSucceed(e *httpexpect.Expect, method string, interval int) {
f, ok := ignoreErrorFuncMap[method]
gomega.Expect(ok).To(gomega.BeTrue())
resp := f(e)
if resp.Raw().StatusCode != http.StatusOK {
for i := range [60]int{} {
timeWait := fmt.Sprintf("wait for %ds\n", i*interval)
fmt.Fprint(ginkgo.GinkgoWriter, timeWait)
resp = f(e)
if resp.Raw().StatusCode != http.StatusOK {
time.Sleep(5 * time.Second)
} else {
break
}
}
}
gomega.Ω(resp.Raw().StatusCode).Should(gomega.BeNumerically("==", http.StatusOK))
}
func RoughCompare(a float64, b float64) bool {
ratio := a / b
if ratio < 1.3 && ratio > 0.7 {
return true
}
return false
}
type silentPrinter struct {
logger httpexpect.Logger
}
func newSilentPrinter(logger httpexpect.Logger) silentPrinter {
return silentPrinter{logger}
}
// Request implements Printer.Request.
func (p silentPrinter) Request(req *http.Request) {
}
// Response implements Printer.Response.
func (silentPrinter) Response(*http.Response, time.Duration) {
}