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

add oauth-proxy to rawdeployments if odh auth label is present #419

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

VedantMahabaleshwarkar
Copy link

@VedantMahabaleshwarkar VedantMahabaleshwarkar commented Oct 9, 2024

What this PR does / why we need it:

This PR adds the following :

  • If the isvc has label "security.opendatahub.io/enable-auth" = "true"
    -- an oauth proxy container is added to the deployment
    -- http port is replaced by https port in the service
    -- "service.beta.openshift.io/serving-cert-secret-name" is added to the service to allow creation of the tls secret
  • Modified the kserve raw deployment spec to use the kserve-sa service account (previously no SA was specified, the default SA was used)
  • This is needed so that oauth proxy can use the kserve-sa service account to perform auth delegation.
  • The service account, CRB and Route are created in Add reconciliation for Kserve Raw odh-model-controller#274

Which issue(s) this PR fixes (optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close the issue(s) when PR gets merged):
Fixes # https://issues.redhat.com/browse/RHOAIENG-10291, https://issues.redhat.com/browse/RHOAIENG-13444
TEST WITH: opendatahub-io/odh-model-controller#274

Type of changes
Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Feature/Issue validation/testing:

  • Deploy ODH/RHOAI:
  • Install the DSC (kserve spec below) :
    kserve:
      defaultDeploymentMode: RawDeployment
      devFlags:
        manifests:
          - contextDir: config
            sourcePath: overlays/odh
            uri: 'https://github.com/VedantMahabaleshwarkar/kserve/tarball/devflags'
          - contextDir: config
            sourcePath: ''
            uri: 'https://github.com/VedantMahabaleshwarkar/odh-model-controller/tarball/devflags'
      managementState: Managed
      serving:
        ingressGateway:
          certificate:
            type: OpenshiftDefaultIngress
        managementState: Managed
        name: knative-serving
  • To test enabling route for a particular ISVC
    -- Create any kserve isvc+SR
    -- isvc should have annotation : "serving.kserve.io/deploymentMode" : RawDeployment
    -- isvc should have label : "networking.kserve.io/visibility": "enable-route"

  • To test route with auth:
    -- Create any kserve isvc+SR
    -- isvc should have annotation : "serving.kserve.io/deploymentMode" : RawDeployment
    -- isvc should have label: "security.opendatahub.io/enable-auth" = "true"
    -- isvc should have label: "networking.kserve.io/visibility": "enable-route"

  • To test Inference without auth:
    -- remove isvc label "security.opendatahub.io/enable-auth" = "true"

Now inference should work without token

  1. Please confirm that if this PR changes any image versions, then that's the sole change this PR makes.

Checklist:

  • Have you added unit/e2e tests that prove your fix is effective or that this feature works?
  • Has code been commented, particularly in hard-to-understand areas?
  • Have you made corresponding changes to the documentation?

Release note:

Added oauth-proxy authentication for Kserve RawDeployment Routes

Re-running failed tests

  • /rerun-all - rerun all failed workflows.
  • /rerun-workflow <workflow name> - rerun a specific failed workflow. Only one workflow name can be specified. Multiple /rerun-workflow commands are allowed per comment.

Copy link

openshift-ci bot commented Oct 9, 2024

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

Copy link

openshift-ci bot commented Oct 9, 2024

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: VedantMahabaleshwarkar

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:
  • OWNERS [VedantMahabaleshwarkar]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci bot added the approved label Oct 9, 2024
@VedantMahabaleshwarkar
Copy link
Author

tests passing locally, the failures look like test infra failures

/retest-required

Comment on lines +142 to +155
# #TODO Remove this until new controller-tools is released
# perl -pi -e 's/storedVersions: null/storedVersions: []/g' config/crd/full/serving.kserve.io_inferenceservices.yaml
# perl -pi -e 's/conditions: null/conditions: []/g' config/crd/full/serving.kserve.io_inferenceservices.yaml
# perl -pi -e 's/Any/string/g' config/crd/full/serving.kserve.io_inferenceservices.yaml
# perl -pi -e 's/storedVersions: null/storedVersions: []/g' config/crd/full/serving.kserve.io_trainedmodels.yaml
# perl -pi -e 's/conditions: null/conditions: []/g' config/crd/full/serving.kserve.io_trainedmodels.yaml
# perl -pi -e 's/Any/string/g' config/crd/full/serving.kserve.io_trainedmodels.yaml
# perl -pi -e 's/storedVersions: null/storedVersions: []/g' config/crd/full/serving.kserve.io_inferencegraphs.yaml
# perl -pi -e 's/conditions: null/conditions: []/g' config/crd/full/serving.kserve.io_inferencegraphs.yaml
# perl -pi -e 's/Any/string/g' config/crd/full/serving.kserve.io_inferencegraphs.yaml
# #remove the required property on framework as name field needs to be optional
# yq 'del(.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.*.properties.*.required)' -i config/crd/full/serving.kserve.io_inferenceservices.yaml
# #remove ephemeralContainers properties for compress crd size https://github.com/kubeflow/kfserving/pull/1141#issuecomment-714170602
# yq 'del(.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.*.properties.ephemeralContainers)' -i config/crd/full/serving.kserve.io_inferenceservices.yaml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is commenting these out intentional?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in update

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are still commented out?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm still seeing comments here...

@@ -414,6 +416,16 @@ const (
SupportedModelMLFlow = "mlflow"
)

// opendatahub rawDeployment Auth
const (
OauthProxyImage = "registry.redhat.io/openshift4/ose-oauth-proxy@sha256:4bef31eb993feb6f1096b51b4876c65a6fb1f4401fee97fa4f4542b6b7c9bc46"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to expose this config? e.g. any updates in the image would require code change in KServe

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't update this oauth proxy image very frequently at all, but do you think I should add it to the configmap and try to consume it from there?

Copy link

@israel-hdez israel-hdez Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better exposing the config, as it allow users quickly moving to another image in case this one becomes vulnerable.

Also, all the other Oauth related constants should be exposed. These should be defaults and users should be capable of customizing, since we don't know how much load there will be which would determine the amount of memory and CPU to be assigned.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, kept these constants as fallback values but PR includes changes to the kserve manifests where the oauth proxy params are defined

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! LGTM

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VedantMahabaleshwarkar
Copy link
Author

/retest-required

@VedantMahabaleshwarkar
Copy link
Author

not sure what's happening with the tests, they are passing locally :(

@@ -1333,7 +1333,6 @@ func schema_pkg_apis_serving_v1alpha1_SupportedModelFormat(ref common.ReferenceC
},
},
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, revert this file.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(bump)

@@ -414,6 +416,16 @@ const (
SupportedModelMLFlow = "mlflow"
)

// opendatahub rawDeployment Auth
const (
OauthProxyImage = "registry.redhat.io/openshift4/ose-oauth-proxy@sha256:4bef31eb993feb6f1096b51b4876c65a6fb1f4401fee97fa4f4542b6b7c9bc46"
Copy link

@israel-hdez israel-hdez Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better exposing the config, as it allow users quickly moving to another image in case this one becomes vulnerable.

Also, all the other Oauth related constants should be exposed. These should be defaults and users should be capable of customizing, since we don't know how much load there will be which would determine the amount of memory and CPU to be assigned.

config/overlays/odh/inferenceservice-config-patch.yaml Outdated Show resolved Hide resolved
pkg/constants/constants.go Outdated Show resolved Hide resolved
`--cookie-secret=SECRET`,
`--openshift-delegate-urls={"/": {"namespace": "$isvcNamespace", "resource": "services", "verb": "get"}}`,
`--openshift-sar={"namespace": "$isvcNamespace", "resource": "services", "verb": "get"}`,
`--skip-auth-regex="(^/metrics|^/apis/v1beta1/healthz)"`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the liveness and readiness probes, instead of being fixed, you should inspect the ISVC and extract the configured paths.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get why we should do this. For the modelmesh oauth proxy injection the oauth proxy probes are fixed. Even here the kserve container and the oauth proxy container will report statuses independently so I'm not seeing why we should change the probes here

Signed-off-by: Vedant Mahabaleshwarkar <[email protected]>
Signed-off-by: Vedant Mahabaleshwarkar <[email protected]>
Signed-off-by: Vedant Mahabaleshwarkar <[email protected]>
Signed-off-by: Vedant Mahabaleshwarkar <[email protected]>
Signed-off-by: Vedant Mahabaleshwarkar <[email protected]>
Signed-off-by: Vedant Mahabaleshwarkar <[email protected]>
@VedantMahabaleshwarkar
Copy link
Author

VedantMahabaleshwarkar commented Oct 17, 2024

the e2e raw test failure is because https://github.com/opendatahub-io/kserve/blob/master/test/scripts/openshift-ci/run-e2e-tests.sh needs to be updated with the new behavior. Imo it can be ignored now can will be fixed later in https://issues.redhat.com/browse/RHOAIENG-14604

Comment on lines +142 to +155
# #TODO Remove this until new controller-tools is released
# perl -pi -e 's/storedVersions: null/storedVersions: []/g' config/crd/full/serving.kserve.io_inferenceservices.yaml
# perl -pi -e 's/conditions: null/conditions: []/g' config/crd/full/serving.kserve.io_inferenceservices.yaml
# perl -pi -e 's/Any/string/g' config/crd/full/serving.kserve.io_inferenceservices.yaml
# perl -pi -e 's/storedVersions: null/storedVersions: []/g' config/crd/full/serving.kserve.io_trainedmodels.yaml
# perl -pi -e 's/conditions: null/conditions: []/g' config/crd/full/serving.kserve.io_trainedmodels.yaml
# perl -pi -e 's/Any/string/g' config/crd/full/serving.kserve.io_trainedmodels.yaml
# perl -pi -e 's/storedVersions: null/storedVersions: []/g' config/crd/full/serving.kserve.io_inferencegraphs.yaml
# perl -pi -e 's/conditions: null/conditions: []/g' config/crd/full/serving.kserve.io_inferencegraphs.yaml
# perl -pi -e 's/Any/string/g' config/crd/full/serving.kserve.io_inferencegraphs.yaml
# #remove the required property on framework as name field needs to be optional
# yq 'del(.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.*.properties.*.required)' -i config/crd/full/serving.kserve.io_inferenceservices.yaml
# #remove ephemeralContainers properties for compress crd size https://github.com/kubeflow/kfserving/pull/1141#issuecomment-714170602
# yq 'del(.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.*.properties.ephemeralContainers)' -i config/crd/full/serving.kserve.io_inferenceservices.yaml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are still commented out?

@@ -414,6 +416,16 @@ const (
SupportedModelMLFlow = "mlflow"
)

// opendatahub rawDeployment Auth
const (
OauthProxyImage = "registry.redhat.io/openshift4/ose-oauth-proxy@sha256:4bef31eb993feb6f1096b51b4876c65a6fb1f4401fee97fa4f4542b6b7c9bc46"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! LGTM

Comment on lines +21 to +25
"encoding/json"
"k8s.io/client-go/kubernetes"
"strconv"

"k8s.io/apimachinery/pkg/api/resource"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imports are not sorted and grouped

// Check if the route is admitted
for _, ingress := range route.Status.Ingress {
for _, condition := range ingress.Conditions {
if condition.Type == "Admitted" && condition.Status == "True" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be a constant from netv1 for "Admitted"

Copy link

openshift-ci bot commented Oct 17, 2024

@VedantMahabaleshwarkar: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-raw 693027b link true /test e2e-raw

Full PR test history. Your PR dashboard.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Comment on lines +142 to +155
# #TODO Remove this until new controller-tools is released
# perl -pi -e 's/storedVersions: null/storedVersions: []/g' config/crd/full/serving.kserve.io_inferenceservices.yaml
# perl -pi -e 's/conditions: null/conditions: []/g' config/crd/full/serving.kserve.io_inferenceservices.yaml
# perl -pi -e 's/Any/string/g' config/crd/full/serving.kserve.io_inferenceservices.yaml
# perl -pi -e 's/storedVersions: null/storedVersions: []/g' config/crd/full/serving.kserve.io_trainedmodels.yaml
# perl -pi -e 's/conditions: null/conditions: []/g' config/crd/full/serving.kserve.io_trainedmodels.yaml
# perl -pi -e 's/Any/string/g' config/crd/full/serving.kserve.io_trainedmodels.yaml
# perl -pi -e 's/storedVersions: null/storedVersions: []/g' config/crd/full/serving.kserve.io_inferencegraphs.yaml
# perl -pi -e 's/conditions: null/conditions: []/g' config/crd/full/serving.kserve.io_inferencegraphs.yaml
# perl -pi -e 's/Any/string/g' config/crd/full/serving.kserve.io_inferencegraphs.yaml
# #remove the required property on framework as name field needs to be optional
# yq 'del(.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.*.properties.*.required)' -i config/crd/full/serving.kserve.io_inferenceservices.yaml
# #remove ephemeralContainers properties for compress crd size https://github.com/kubeflow/kfserving/pull/1141#issuecomment-714170602
# yq 'del(.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.*.properties.ephemeralContainers)' -i config/crd/full/serving.kserve.io_inferenceservices.yaml

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm still seeing comments here...

@@ -2,3 +2,4 @@ kserve-controller=quay.io/opendatahub/kserve-controller:latest
kserve-agent=quay.io/opendatahub/kserve-agent:latest
kserve-router=quay.io/opendatahub/kserve-router:latest
kserve-storage-initializer=quay.io/opendatahub/kserve-storage-initializer:latest
oauth-proxy=registry.redhat.io/openshift4/ose-oauth-proxy@sha256:4bef31eb993feb6f1096b51b4876c65a6fb1f4401fee97fa4f4542b6b7c9bc46

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -1333,7 +1333,6 @@ func schema_pkg_apis_serving_v1alpha1_SupportedModelFormat(ref common.ReferenceC
},
},
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(bump)

`--cookie-secret=SECRET`,
`--openshift-delegate-urls={"/": {"namespace": "` + namespace + `", "resource": "inferenceservices", "group": "serving.kserve.io", "name": "` + isvc + `", "verb": "get"}}`,
`--openshift-sar={"namespace": "` + namespace + `", "resource": "inferenceservices", "group": "serving.kserve.io", "name": "` + isvc + `", "verb": "get"}`,
`--skip-auth-regex="(^/metrics|^/apis/v1beta1/healthz)"`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up #419 (comment).

These are not the probes, but paths that would be left unprotected.

Now... I'm thinking that you are right on your comment that Kubernetes is going to do the probing on the container. With that in mind, I'm not sure why we should have unprotected paths. So, maybe, remove the --skip-auth-regex argument?

`--upstream=http://localhost:` + upstreamPort,
`--tls-cert=/etc/tls/private/tls.crt`,
`--tls-key=/etc/tls/private/tls.key`,
`--cookie-secret=SECRET`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, follow-up my comment of the previous review round: #419 (comment)

routeReady := false
route := &routev1.Route{}
err := cli.Get(context.TODO(), types.NamespacedName{Name: isvc.Name, Namespace: isvc.Namespace}, route)
if err != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle appropriately the not found error.

// Construct the URL
host := route.Spec.Host
isSecure := false
if isSecure, err = strconv.ParseBool(isvc.Labels[constants.ODHKserveRawAuth]); err != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly better to check if route.Spec.tls.termination is populated

if val, ok := componentMeta.Labels[constants.InferenceServiceLabel]; ok {
isvcname = val
}
service.ObjectMeta.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = isvcname

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reconciler is used for predictors, explainers, transformers and also the inference graph. So, it is going to be neccesary to use the name as is in componentMeta.Name rather than the name of the inference service. Otherwise, you will have several Services generating different certificates, for different FQDN, and trying to store such certificate to the same Secret, leading to conflicts.

Additionally, I'd recommend, adding a suffix like -serving-cert .

Comment on lines +156 to +166
ports := service.Spec.Ports
replaced := false
for i, port := range ports {
if port.Port == constants.CommonDefaultHttpPort {
ports[i] = httpsPort
replaced = true
}
}
if !replaced {
ports = append(ports, httpsPort)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From

if len(container.Ports) > 0 {
var servicePort corev1.ServicePort
servicePort = corev1.ServicePort{
Name: container.Ports[0].Name,
Port: constants.CommonDefaultHttpPort,
TargetPort: intstr.IntOrString{
Type: intstr.Int,
IntVal: container.Ports[0].ContainerPort,
},
Protocol: container.Ports[0].Protocol,
}
servicePorts = append(servicePorts, servicePort)
for i := 1; i < len(container.Ports); i++ {
port := container.Ports[i]
if port.Protocol == "" {
port.Protocol = corev1.ProtocolTCP
}
servicePort = corev1.ServicePort{
Name: port.Name,
Port: port.ContainerPort,
TargetPort: intstr.IntOrString{
Type: intstr.Int,
IntVal: port.ContainerPort,
},
Protocol: port.Protocol,
}
servicePorts = append(servicePorts, servicePort)
}
} else {
port, _ := strconv.Atoi(constants.InferenceServiceDefaultHttpPort)
servicePorts = append(servicePorts, corev1.ServicePort{
Name: componentMeta.Name,
Port: constants.CommonDefaultHttpPort,
TargetPort: intstr.IntOrString{
Type: intstr.Int,
IntVal: int32(port), // #nosec G109
},
Protocol: corev1.ProtocolTCP,
})
}
, looks like the CommonDefaultHttpPort entry should be present always, and it is going to be the first entry always. So, I think you can simply mutate service.Spec.Ports[0] and change the target port...

...sorry, I know that I said about being defensive in my previous comment.

`--provider=openshift`,
`--skip-provider-button`,
`--openshift-service-account=kserve-sa`,
`--upstream=http://localhost:` + upstreamPort,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the upstream is always configured to the predictor container (the runtime). But when observing here:

if componentExt != nil && componentExt.Batcher != nil {
servicePorts[0].TargetPort = intstr.IntOrString{
Type: intstr.Int,
IntVal: constants.InferenceServiceDefaultAgentPort,
}
}
if componentExt != nil && componentExt.Logger != nil {
servicePorts[0].TargetPort = intstr.IntOrString{
Type: intstr.Int,
IntVal: constants.InferenceServiceDefaultAgentPort,
}
}

I can see that the Service is choosing between the agent container and the predictor container. Since when auth is enabled, that Service will be moved to oauth-proxy, there should be an equivalent logic here, so that oauth-proxy chooses between the predictor and the agent container.

By setting it fixed to the predictor, the agent container can be bypassed, leading to batching and logging features to stop working when enabling auth. The logging feature is being used by TrustyAI team, so I think we should ensure logging still works.

@@ -1264,6 +1270,109 @@ var _ = Describe("v1beta1 inference service controller", func() {
Eventually(func() error { return k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA) }, timeout).
Should(HaveOccurred())
})
It("Should have no ingress created if labeled as cluster-local", func() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, contribute this one to upstream?

@@ -272,6 +272,8 @@ var _ = Describe("v1beta1 inference service controller", func() {
FSGroupChangePolicy: nil,
SeccompProfile: nil,
},
ServiceAccountName: constants.KserveServiceAccountName,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing right to change the default service account. It could be considered backwards incompatible.

expectedService := &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: predictorServiceKey.Name,
Namespace: predictorServiceKey.Namespace,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add unit tests for the routes.

ports := service.Spec.Ports
replaced := false
for i, port := range ports {
if port.Port == constants.CommonDefaultHttpPort {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the CommonDefaultHttpPort is going away, I think you should reflect in the Status of the ISVC the right port to reach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: New/Backlog
Development

Successfully merging this pull request may close these issues.

4 participants